Live data from Hacker News

Making a Go program faster with a one-character change

hmarr.com

111–120 of 249 posts

Re: Making a Go program faster with a one-character change

#111
post #96

Earlier quoted context omitted.

> When I write code, it's common to have references to immutable classes thrown around with wild abandon, heedless of ownership, threads, or good taste, because the data just can't change. If there's anything I wish languages with implicit reference semantics would adopt, it's implicit immutability. I wish Java would be so much nicer with keyword that is half way between "final" and "volatile" that means, "yes, you c…

Agreed. Could you Imagine a Java where you have a `Map` and a `MutableMap` and that's what you put at your API? I'd make it SO much clearer how safe any individual API is to call.

Kotlin has this

Re: Making a Go program faster with a one-character change

#112

Earlier quoted context omitted.

This is an extremely common mistake in reporting performance numbers. That the old version is 70% slower does not make the new version 70% faster.

70% slower is a bit ambiguous though - it could mean 70% extra runtime or it could mean 30% of the new speed. Whereas 70% faster would always suggest to me that it can do 70% more work in the same amount of time, i.e. a 1.7x increase in speed.

I do not agree. When you benchmark you usually measure the differences between times needed to complete. This is because it is highly non-obvious that if you increase workload twice, the time increases also twice. Perhaps the algorithm is not linear. Perhaps if you have more data, you suddenly need to swap memory. Perhaps something (like disk access in parallel) means that actually it takes less than 2x time. This means that a concept of speed per unit of work is undefined. So the only reasonable interpretation of "70% faster" means "spends 30% time of original".

Re: Making a Go program faster with a one-character change

#113

Earlier quoted context omitted.

It's the difference between assigning/passing around "copies of the data" vs. assigning/passing around "the memory address for that data" under the hood. PHP, for example, has explicit references. If you have an `$arr1=array(1,2,3)` and an `$arr2 = $arr1`, that second array is a full copy of the first array, and updating $arr1 does nothing to $arr2. Similarly, `function update_array($arr) { $arr[0] = 'cake'; }` calle…

That's not technically correct with regards to PHP. Your statement that any changes to $arr1 or $arr2 only impact the one in question, however, is accurate. If no changes are made they still refer to the same data in memory. It's copy-on-write semantics. $arr1 = [1,2,3]; // $arr1 is a pointer to a zval array [1,2,3] and refcount:1 $arr2 = $arr1; // $arr1 and $arr2 are pointers to the same zval array but incremented r…

Actually that is outdated since at least PHP 7. In modern PHP the engine uses copy on write only for "larger" things, but "small" things like integers or booleans live on the stack and are copied around, avoiding heap allocations etc.

Re: Making a Go program faster with a one-character change

#114

Earlier quoted context omitted.

Agreed. Could you Imagine a Java where you have a `Map` and a `MutableMap` and that's what you put at your API? I'd make it SO much clearer how safe any individual API is to call.

Kotlin has this

Kotlin has this, but the Map is (usually) a MutableMap under the covers, because it's Java bytecode at the lower levels. You have to go out of your way to footgun yourself, but it's still possible.

Re: Making a Go program faster with a one-character change

#115
post #30

Earlier quoted context omitted.

> perhaps the problem is implicit allocations rather than value semantics? I think that’s true. Expensive copies should never have been implicit. There was a story some time ago about a single keypress in the address bar of Chrome causing thousands of memory allocations. The culprit: lots of std::string arguments up and down the call stack. Rust gets this right, with the hindsight of C++’s example: “a = b” is a move…

> except for plain data types where copying is literally memcpy what do you mean by this? If I say `let x = 5; let y = x;` in rust, that's a "plain data type copy" of a stack value, but memcpy is usually used to copy heap memory. What connection between copying of primitive simple stack values and memcpy are you suggesting here?

The compiler can optimize memcpy with a known size into a small number of move instructions so they are identical to copying stack values.

Try playing with memcpy on Godbolt and you'll find that the compiler will compile the memcpy to a single mov instruction when the size is small, and some movdqu/movups when the size is slightly large, and only a function call when the size is huge.

> memcpy is usually used to copy heap memory

memcpy is often used in low-level serialization / deserialization code since you can't just cast a buffer pointer to a uint32_t pointer and dereference that; the solution is memcpy between variables that are often both on the stack.

Re: Making a Go program faster with a one-character change

#116
post #54
post #21

As an old C/C++ programmer, I'm always surprised by how often software developers are surprised by the performance costs of inopportune value semantics (C and C++ even more so, punishes you severely for using value semantics when you shouldn't). I increasingly see the wisdom of languages with implicit reference semantics. It's not that value semantics can't be better (they most assuredly can be), or that reference se…

I would also lay a chunk of blame on the use of type inference which causes the information about the behaviour to be hidden from view. Had the author been looking at the type information within the syntax of the code the profile output may not have been a surprise. Perhaps the problem would never have existed in the first place.

Yeah. Often, writing out the type explicitly is just busywork. But it seems it would have paid off here.

If you were forced to stop and think what type to declare, I bet you'd write "var rule *Rule". Even if you don't think deeply and just look at the return type.

And then if you assigned "r[i]" to "rule", you'd get a type error.

Re: Making a Go program faster with a one-character change

#117

There is potentially another option: use the midstack inliner to move the allocation from the heap to the stack of the calling function: https://words.filippo.io/efficient-go-apis-with-the-inliner/ As long as the global slice is never mutated, the current approach is probably fine, but it is definitely a semantic change to the code.

That seems like overkill for this particular case, but it's a very interesting technique, thanks for the link!

Re: Making a Go program faster with a one-character change

#118

Earlier quoted context omitted.

> Implicit allocation isn't really a problem because a runtime that optimizes the allocations magically for you is a lot easier to build As far as I know, Java's (default) runtime gives cheap allocations at the cost of long GC pause times. > than a runtime that optimizes whether you really need to be copying objects as much as you do It's not "copying", it's "allocating", and avoiding allocations isn't that much work…

> As far as I know, Java's (default) runtime gives cheap allocations at the cost of long GC pause times. "long GC pause times" is kind of vague, so I guess you could be correct, but in practice there's a LOT of different ways the memory management can be handled, many of which are deemed "pauseless GC" (though the term is somewhat misleading). My statement was considering that reality though. While not true for some…

> in practice there's a LOT of different ways the memory management can be handled, many of which are deemed "pauseless GC" (though the term is somewhat misleading).

Yes, but I'm pretty sure those "pauseless GC" schemes impose other tradeoffs.

> My statement was considering that reality though. While not true for some use cases, in the vast majority of cases, the runtime optimizes the allocations more than sufficiently.

I'm not sure I follow. The same could be said for Go--in the vast majority of cases, Go's tradeoffs (slow allocations, low latency / non-moving GC) are also suitable.

> Allocators can do a pretty good job of minimizing the overhead of allocation, to the point the amortized cost isn't much more than a single machine instruction.

As far as I know, speeding up allocations to this degree requires a moving GC which imposes a bunch of other constraints (including copying a bunch of memory around).

> Allocating gigabytes of memory quickly is possible. Copying the data can be a lot more work, and often objects have copy semantics that add a lot more additional work.

Yes, but the bottleneck here wasn't the copying, it was the allocations. And if you optimized away allocation cost entirely such that only the copy cost remained, that cost would be so small that the OP would never have bothered to profile because copying small objects like this is so cheap compared to everything else (even if it is expensive compared to bump allocating).

> I think you're implicitly saying "a runtime that minimizes heap allocations" there, in which case I'd agree.

Yes, the allocator and GC are concerned with heap allocations and not stack allocations. I'm using "allocations" as a shorthand for "heap allocations".

Re: Making a Go program faster with a one-character change

#119
post #96

Earlier quoted context omitted.

> When I write code, it's common to have references to immutable classes thrown around with wild abandon, heedless of ownership, threads, or good taste, because the data just can't change. If there's anything I wish languages with implicit reference semantics would adopt, it's implicit immutability. I wish Java would be so much nicer with keyword that is half way between "final" and "volatile" that means, "yes, you c…

Agreed. Could you Imagine a Java where you have a `Map` and a `MutableMap` and that's what you put at your API? I'd make it SO much clearer how safe any individual API is to call.

In general this doesn't work, the history rule says mutable types are not proper subtypes of immutable ones (and the converse is obvious). If you want to capture mutability in your type system, it needs to be orthogonal to subtyping (like C/C++ const).

Re: Making a Go program faster with a one-character change

#120

So, this is very basic Go design and you could write something about how it works in C and Go and why a older lang like C don't have this prob but then at the end of the day the Go fanclub will down vote the hell out you no matter what.

Go compiler is garbage by the design. A 20 year old C compiler does not have this prob. This is also why Go have declined so much during the last couple of years. The benefits of Go have not increased and most of the quirks are still there. Like the error handling, the naive compiler and the syntax sugar that somewhat hides the diff between pointers and direct heap allocs.

-1

Post reply on HN