Live data from Hacker News

Making a Go program faster with a one-character change

hmarr.com

211–220 of 249 posts

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

#211

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…

> It's copy-on-write semantics.

CoW is not semantics, it’s a way of implementing value semantics which avoids unnecessary defensive copies.

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

#212
post #17

Earlier quoted context omitted.

BTW; I'm using both Go and Rust lately. In Rust you can write a function that returns the pointer of one element of a slice. You can also write a function that returns the pointer to a heap-allocated copy of an element of the slice. The two functions would have different signatures. The compiler would also prevent mutation of the slice as long as there are any references to individual elements of the slice being pass…

>In Rust you can write a function that returns the pointer of one element of a slice. Have fun fighting the borrow checker on that one.

Yes and that's the whole point!

The borrow checker ensures that you either do it right or you find another way that is safe.

For example it may be totally fine to help allocate a result, not all programs need to be optimized to the bone. Just return a boxed value. If you need to share it, wrap it in an Rc or Arc.

One problem I see with it is that rust chooses to make the most efficient way of programming also the most "simple looking", and so it lines up incentives in such a way that people will unnecessarily try to avoid using an extra Arc just because it looks like unnecessary clutter.

When you learn to embrace your boxing you'll learn that the borrow checker is not your enemy.

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

#213
post #139

Earlier quoted context omitted.

Sometimes it doesn't matter if a public API is incorrect, because it's set in stone for whatever reason, and you just need to fix the problem internally.

The way to fix it in that manner here is to undo the 42% speedup and return the heap allocated object for the caller to mangle.

Yes, if you are serious about backwards compat then you pay that cost and perhaps add a separate function that is faster for callers to opt into.

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

#214
post #174

Earlier quoted context omitted.

Not necessarily a bad thing either, things can get odd if you're not the only owner and it's mutated under you unexpectedly.

That only matters if you're storing it. The big issue is 99% of code out there uses mutable template types for containers, and if you ever declare a container that doesn't have a mutable template type, you stub your toe as your new container isn't compatible with anyone else's code. You can't even easily copy your way out. std::unordered_map m; m["foo"] = "bar"; std::unordered_map m2 = m; doesn't compile.

> That only matters if you're storing it.

Or if you (or something you call) mutate it trough another reference/pointer. Sometimes I wish there was a stronger const where the compiler (and programmer) could actually assume the object is not mutated while that reference/pointer is alive. Of course checking that is no longer doable with just the type system.

> You can't even easily copy your way out.

That depends on the type. std::unordered_maps could provide a copy constructor that does the conversion.

And you can do the copy via iterators, although that is probably not as efficient as it could be because it needs to re-hash the keys.

  std::unordered_map m2(m.begin(), m.end());

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

#215

Earlier quoted context omitted.

> you can't const-cast the templated const away. That seems like a good thing. If you're handed a map to const values you can't just go "imma gunna mutate them anyway".

Normally (when containers are not involved) this is exactly the point of a const cast.

No, the primary point of a const cast is "I need to pass this to an API that expects a non-const pointer even though it won't mutate it".

Casting const away and then mutating is a footgun as you are in undefined behavior territory as soon as your pointer/reference is not just const itself but points to a const variable.

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

#216
post #41

Earlier quoted context omitted.

The regular return value doesn’t have to be meaningless just because the error is non-nil. In this case the function returns the rule that triggered the error, which is potentially useful information. I don’t think it is an official Go convention that err being non-nil entails that the other return value should be meaningless.

It's quite error prone, most golang code is if err != nil { return nil, err } Now of course it's important to read the documentation, but a language with sum types (or exceptions) would have used a separate type to indicate an error condition plus useful information on that error.

Right, but Go doesn't have sum types, and I think it's a mistake to interpret Go's error handling conventions through that lens. It's perfectly fine to return an error together with a meaningful value. At least, I haven't been able to find any official Go docs suggesting otherwise. You might wish that Go had sum types, but that doesn't mean that it's an actual Go coding convention to pretend that (err, X) tuples are sum types.

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

#217
post #51

Earlier quoted context omitted.

I write a lot of Go, and I agree that this is a big wart in its error handling that would be served by a proper Result type. Nevertheless, the convention is that if a function returns (value, err), and err != nil, the value is discarded (I think of it as "undefined"). So the code is conventional.

In C, “discarding” a pointer in a way that leaves the value visible is quite common. At least if one doesn’t accidentally use the pointer, it’s harmless. (In the way that all manner of unsafeness is harmless in C as long as no actual UB occurs, which is to say it’s not great.) But Go is a garbage collected language, and there is so such thing as “discarding” a pointer. Either it’s there or it isn’t, and this kind of…

>and this kind of leak has side effects

Only if the calling function does something with the pointer (which it generally won't, if err is non-nil). If the calling code does do something with the pointer even when err is non-nil, then either

(a) the value of the pointer is meaningful, and this is fine; or

(b) there's a logic error in the calling code, which is a far more serious bug than a potential memory leak.

So I don't really see the problem here.

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

#218

Earlier quoted context omitted.

I think this is half right. For anything 64 bits or smaller, value semantics are pretty much always going to be better. That said, being able to choose between value and reference semantics for larger objects per object is a pretty useful feature.

> For anything 64 bits or smaller, value semantics are pretty much always going to be better. That's assuming a 64-bit CPU (which admittedly seems like a reasonable assumption. The nice thing about the abstraction though is that there's nothing preventing the runtime from applying value semantics for those trivial small-object cases where they're obviously more efficient.

Even for a 32-bit CPU a 64-bit type is only two words to copy - and in many cases those "copies" are just register loads. In contrast, reference types means to even access it you have to read the reference and then indirectly load the memory it points to. You have to really make something contrived where a two-word type ends up being more efficient as a reference than as a value.

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

#219
post #134

Earlier quoted context omitted.

I really hate when people use "errors" for signals. Or, alternately, use the signals mechanisms for actual errors. An error should be "something went wrong", and reading a file to the EOF is not "going wrong", that's just what the "read()" should do if you tell it so! I like Go's multiple returns and error checking by default, but it definitely should have been implemented with some sort of "Result/Error" type union…

If you take the position that the semantics of read() are 'read data from this endless stream', then EOF is an error indicating that this model no longer applies. Doesn't bother me at all.

Except in that model it would be fine to return an error as soon as soon as you know there is an EOF while in the real world you do want to read right up to the EOF because EOF is usually not an error condition but expected, even with streams of indeterminate length.

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

#220
post #30

Earlier quoted context omitted.

I also have a background in C/C++, etc and I've only ever found myself missing value semantics when I use languages with implicit reference semantics. I guess I always figured the solution was "value semantics with better education / tooling". Education: people should understand value semantics. Tooling: imagine an IDE that highlights allocation points automatically (or perhaps the problem is implicit allocations rat…

> 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…

> Rust gets this right, with the hindsight of C++’s example: “a = b” is a move operation by default and clone() is always explicit

Note that a move can still do a copy; in fact, Rust is kinda notorious for generating more on-stack memory copy operations than C++. It’s slowly improving, but it can still be surprisingly bad in some cases.

Post reply on HN