9 ms·
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
by cbsmith 4y ago
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 semantics don't cause their own complexity problems, but rather that so often we thoughtlessly imply/impose value semantics through interfaces in ways that negatively impact performance; getting interfaces wrong is a much tougher bell to unring.
The vast majority of my mental energy when I define an interface in C++ is carefully thinking through a combination of ownership contracts and value vs. reference semantics that I can mostly ignore in languages with implicit reference semantics. While occasionally ignoring those contracts while developing in Java/Python/whatever comes back to bite me, the problem isn't nearly as common or problematic as when I unintentionally impose value semantics in a language that allows me to.
- throwaway894345 4y agoI 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 rather than value semantics?).
- TremendousJudge 4y ago>or perhaps the problem is implicit allocations rather than value semantics To me, this sounds like this is it. Explicit is better than implicit is a very useful truism
- cbsmith 4y agoThe counter argument to the "explicit is better than implicit" is that abstraction & encapsulation are such significant force multipliers. If done properly, implicit is good. It's just that in case of copying, doing it "properly" is well nigh impossible.
- kazinator 4y agoexplicit implicit good * < * v v v bad * > * Good implicit is better than good explicit. (If all is good, go for implicit.) Bad explicit is better than bad implicit. (If all is bad, go for explicit; don't hide bad explicit with bad implicit.) Good explicit or implicit is better than bad explicit or implicit.
- codeflo 4y ago> 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 operation by default and clone() is always explicit, except for plain data types where copying is literally memcpy — and those are clearly marked as such by the type system.
- cbsmith 4y agoIMHO, implicit allocations is a bit of a red herring. Yes, in C/C++ heap allocations are proportionately pretty expensive, but I've seen Java programs have just ridiculous amounts of implicit allocations but there really isn't much of a problem. But allocations aren't the same as copies, and the argument for reference semantics has always been that implicit copies are problematic. In your std::string example, having that many String copies in a Java program would be similarly terrible (and this sometimes happens by accident because of abstraction layers that hide all the copying going on under the covers). I do think Rust gets a lot of stuff right, but Rust's cognitive load is broadly recognized. I tend to see it as C++ with a lot fewer foot guns. ;-)
- codeflo 4y agoI’m not sure I get what you mean. You wouldn’t have that many String copies in Java by passing an unchanged String down the call stack. My point is that it’s too easy to make this mistake in C++.
- cbsmith 4y agoIn Java, the mistake happens only when there's an abstraction that hides the copying from you, so it isn't implicit in the same way, but it's still implicit.
- morelisp 4y agoAllocations are as damaging as your free function is slow. Java has a tremendously good GC, so can cope with lots of allocations. Go has an OK one, so needs some help (but mollifying it often pays dividends elsewhere in locality and memory usage too). C++ has your default system heap, good luck.
- cbsmith 4y ago> I've only ever found myself missing value semantics when I use languages with implicit reference semantics. Oh, I miss it every time. ;-) I will say though that some newer languages seem to have a confused idea about how to offer mixed semantics. A bunch of them tie semantics to types. The ideal interface can vary by usage context. It's hard enough getting the semantics right as the callee (as opposed to caller), let alone when you're defining a type that will be used by who knows how many interfaces. > I guess I always figured the solution was "value semantics with better education / tooling". I've always thought much the same, but I have slowly come to appreciate that it's more than just education & tooling. Even with good education & tooling, there's a cognitive load that comes with getting interfaces right that for the general case is just not worth it.
- adgjlsfhk1 4y agoI 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.
- cbsmith 4y ago> 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.
- account42 4y agoEven 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.
- Serow225 4y agoThe JetBrains IDEs can do this, at least for .NET
- pjmlp 4y agoVS did it first with Roslyn plugins.
- ok123456 4y ago> Tooling: imagine an IDE that highlights allocation points automatically Rider does this already for C#.
- dleslie 4y agoI 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.
- adrianmonk 4y agoYeah. 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.
- Jemaclus 4y agoI'm a dumb dumb. Can you define "implicit reference semantics" and "value semantics"? You use the phrases several times in your post, but I don't really understand what you mean. If it helps, I'm not a C++ programmer, but I am familiar with higher level languages like Go, Python, Ruby, PHP and Javascript.
- masklinn 4y ago"Implicit reference semantics" = everything (or near enough) is implicitly a reference. That's what you get in Python, Ruby, or Javascript: when you pass something around, you're really passing a reference to it, a pointer. Everyone ends up referring to the same thing, and sharing it. "Value semantics": you pass the value itself around, which means you're shallow-copying it all the time. That's what you get when you pass or return a bare non-interface type in Go. PHP is a bit of a mess, objects are passed by reference (= implicit reference semantics) but arrays are passed by value, and you can opt them into pass-by-reference. You can also pass non-arrays by reference, though I don't think that's very common.
- strifey 4y agoIf you're familiar with Python and Go, you'll likely be able to quickly spot the differences in how they handle parameter passing. Python uses references and Go uses value.
- erik_seaberg 4y agoGo maps and channels have reference semantics. Slices are passed and returned by value, but backing arrays are often shared (not copied) in an error-prone way, so it’s safest to pretend they have move semantics and treat only one copy as valid. IMHO they should have used pointers everywhere they didn’t want value semantics with deep copies.
- klodolph 4y agoLanguages like Python, Java, and C# have implicit reference semantics. When you create an object, you get a pointer to that object (usually, or commonly). In languages like C, C++, Go, Rust… references are more explicit. If you want a pointer to an object, you have to &, or something similar. It gets a bit fuzzy.
- deleted 4y ago[deleted]
- dimitrios1 4y agoAs a current Go programmer, hunting down escaping pointers via escape analysis is a common part of the routine now.
- brundolf 4y agoHaving used C++ relatively little, and a long time ago (and never used Go), I don't think I ever realized that value semantics could be used to implicitly clone heap objects Yeesh
- masklinn 4y agoNormally they’re not, that’s really specific to C++ nonsense. In a normal value-semantics system if you have a pointer you just copy the pointer. Obviously if the langage doesn’t have your back it also means you’ve now fucked up your ownership, but you’re in C so that was to be expected.
- brundolf 4y agoYeah, this was my assumption when I read the original comment, so I was confused about what it was saying (I thought it was suggesting you should pass references instead of doing a bitwise-copy most of the time, which is just plausible enough that I believed it for a minute) until I read further down in the thread and realized they meant implicit heap-cloning
- deleted 4y ago[deleted]
- titzer 4y agoI think the main issue in C++ isn't value semantics, it's deep copy semantics. E.g. in a functional language ADTs are immutable and don't have identity. They can be freely copied, or not, passed by reference or by value, but they are never deep copied. Comparison may be deep, but not passing them. That is to say, I think I mostly am agreeing with you. In Java, objects are always passed by reference, never by value, and never implicitly copied. But Java doesn't have any value types other than primitives. When I added ADTs to Virgil, I wanted to get the best of both worlds; objects are pass by reference, like Java, and ADTs are immutable, identity-less, so they are deep compared but never deep copied. (ADTs in Virgil can also sometimes be flattened, so they don't necessarily result in heap allocation).
- cbsmith 4y agoIn a functional language, you don't have to worry about bits of code mutating your data. ;-) On the flip side, there's a lot of cognitive load that comes with functional languages, so while they do address the problem neatly... I'd have to take a look at Virgil to appreciate your approach, but I'm always leery of implicit value vs. reference semantics tied to types (aside from the whole array fiasco, easily the ugliest part of Java's type system). So often the particular semantics you want are driven by the context of what you're doing, rather than the what you're doing it with.
- nyanpasu64 4y agoI still don't see why value structs need to be immutable; ints are mutable in all languages, and structs are mutable in C, C++, and Rust (if you `let mut`) and it's a feature of the language.
- thomascgalvin 4y ago> I increasingly see the wisdom of languages with implicit reference semantics. I spend most of my time in a JVM language of one flavor or another, and when I was learning Go, the first thing that stuck out at me was, "why would I ever want the compiler to invisibly copy a data structure for me?" I suppose the primary reason is to prevent the callee from modifying the caller's data out from under them; unless you pass a reference value, you know the callee cannot modify your data. But, as someone who leans heavily into "everything should be as immutable as possible," the second thing that stuck out at me was "wait, a struct can't have const fields?" 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. But that's a paradigm that Go simply doesn't support.
- titzer 4y ago> 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. But that's a paradigm that Go simply doesn't support. You might get a kick out of Virgil. It's easy (and terse!) to define immutable classes and you can have immutable ADTs too. (plus tuples, generics with separate typechecking, etc).
- cbsmith 4y ago> 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 can actually mutate this" and then make final semantics the default for fields & variables.
- cogman10 4y agoAgreed. 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.
- bitexploder 4y agoBecoming good at Go is mostly knowing all the sharp edges with the built in types IMO. Of course go routines and concurrency primitives are difficult to master, but that is a different beast and if you understand concurrency from some other languages Go just makes that easy. But knowing all the behaviors of slices, and really intuiting how they work makes your life a lot easier and a lot less prone to bugs. And slice behavior almost all comes down to what is a slice internally and how and where am I copying things as I wrote my code. Generally the copying is fine, but in some cases it is not or it is in a tight performance critical section where you need to be thinking about it. Esit: Also, pointers into slices will probably leave you sad. You get a pointer to the slice storage, not a pointer to the thing. And if the slice resizes your pointer mow references a dead slice. Basically, pointers and slices are not friends. Unless you have a slice of pointers, which idiomatic go avoids unless there is a decent reason for it :)
- tylerhou 4y agoThe performance issue here is not value semantics, it's the overhead of automatic lifetime management. The copy is cheap. The lifetime tracking is not because it forces a heap allocation and creates additional GC pressure. In fact, assuming Rule is small, if Match returned by value, the code would be similarly as fast.
- cbsmith 4y ago> The performance issue here is not value semantics There's no performance cost to value semantics, so of course not. > The copy is cheap. The lifetime tracking is not because it forces a heap allocation and creates additional GC pressure. In fact, assuming Rule is small, if Match returned by value, the code would be similarly as fast. I'm referring more to how this stuff seeps in without the programmer realizing it. It's the implicit nature of all this behaviour that is the problem.
- tylerhou 4y agoSorry, because of value semantics. (Also, you wrote "performance costs of inopportune value semantics". So the correction is annoying; you know what I meant.) This stuff seeps in without the programmer realizing it because Go made a deliberate design decision to have automatic lifetime management. In other words, this is a feature of the language and not a bug. The only way for this to not seep in is if Go forced programmers to specify most lifetimes, which would make the language much more cumbersome to use. I.e., this is not a value-vs-reference semantics issue. It's a manual-lifetime-management vs automatic-lifetime-management issue. The solution is to either 1) write in a language with more explicit lifetimes if performance is that important, or 2) profile your code and reduce heap allocations, which is what the person who wrote the article did.
- cbsmith 4y agoSorry for being annoying. I wasn't trying to be. I would disagree about this stuff seeping in because of automatic lifetime management. The equivalent code in Java, Smalltalk, etc., would either not have the performance problem or it would be much more obvious that the code wasn't as efficient as it could be.
- deleted 4y ago[deleted]