4 ms·
> The bytes.Reader should really implement Peek. I’m pretty sure the reason it doesn’t is because this is the only way of creating read only views of slices. An
by msteffen 1y ago
> The bytes.Reader should really implement Peek. I’m pretty sure the reason it doesn’t is because this is the only way of creating read only views of slices. And a naughty user could peek at the bytes and then modify them. Sigh. People hate const poisoning, but I hate this more.
When I was a Google, a team adjacent to ours was onboarding a new client with performance demands that they could not realistically meet with anything resembling their current hardware footprint. Their service was a stateless Java service, so they elected to rewrite in C++. Now, Java has some overhead because of garbage collection and the JVM, and they hoped that this might move the needle, but what happened was they went from 300qps/core to 1200, with lower tail latency. Literally 3x improvement.
Why? Probably a lot of reasons, but the general consensus was: Java has no const, so many of Google’s internal libraries make defensive copies in many places, to guarantee immutability (which is valuable in a highly concurrent service, which everything there is). This generates a huge amount of garbage that, in theory, is short-lived, rarely escapes its GC generation, and can all be cleaned up after the request is finished. But their experience was that it’s just much faster to not copy and delete things all over the place. Which you can often avoid by using const effectively. I came to believe that this was Java’s biggest performance bottleneck, and when I saw that Go had GC with no const, I figured it would have the exact same problem
- hinkley 1y agoJava has a little const. Strings are immutable. You can make objects with no mutations, so you can make read only collections fairly easily which is usually where const becomes a problem. But then you have for instance Elixir, where all functions are pure, so mutating inputs to outputs takes a ton of copying, and any data structure that is not a DAG is a gigantic pain in the ass to modify. I lost count of how many tries it took me to implement Norvig’s sudoku solver. I kept having to go back and redesign my data structures every time I added more of the logic. [edit to add]: DTOs exist in Java because some jackass used the term “Value Object” to include mutation despite large swaths of the CS world considering VOs to be intrinsically const. So then they had to make up a new term that meant Value Object without using the concept they already broke.
- throwawaymaths 1y agoThere are so many purity escape hatches in elixir!!
- hinkley 1y agoThe only ones I know about are the ones in functions and closures, where SSA basically just creates a new variable that hides the original (until the end of the block at which point you discover the reassignment didn't stick). What did you have in mind?
- throwawaymaths 1y agoets tables are the goto for data structures (for example the :digraph module that ships with elixir is built on an ets table, presumably because A* needs a mutable datatype) Process dictionary is also an option. Can always use a genserver as a data store (but only do that if lifetime shenanigans make it make sense) Postgres is also an option. Sqlite if you don't want to stand up a service.
- hinkley 1y agoAh yes. I’m using a genserver to persist a countdown clock across page loads. ETS gives me a memory leak vibe but I need to bite the bullet and learn to use it properly.
- throwawaymaths 1y agoThink of ets as an arena. You can clear the whole table by yanking its owning process. This could even be made automatic by linking it to whatever process needs the ets table.
- hinkley 1y agoHmmm. Maybe I’m misremembering some Erlang release notes then.
- hnlmorg 1y agoAre you able to explain the problem a little more because “const” does exist as a keyword, so I assume it’s doing something different to what you’re referring to with regards to C++ constants. Is Go not substituting constants like a macro? Or are we discussing something entirely different and I’m misunderstanding the context here?
- kentm 1y agoI’m assuming they mean const function/method parameters. Being able to mark inputs to functions as const to guarantee that they aren’t mutated in C++ which often means you can just pass in the value by reference safely.
- jerf 1y agoJava, Go, and C++ all have enough differences here that at this level of detail you shouldn't assume any other conversion will have exactly the same result that msteffen lays out. Java generally has more sophisticated compilation and a lot more engineering effort poured into it, but Go often ends up with less indirection in the first place and started life with what Java calls records [1] so they are more pervasive throughout the ecosystem. Which effect "wins" can be difficult to guess in advance without either a deep analysis of the code, or just trying it. What msteffen talks about is a general principle that you can expect even small differences between languages to sometimes have significant impact on code. I think this is also one of the reasons Rust libraries tend to come out so fast. They're very good at not copying things, but doing it safely without having to make "just in case" copies. It's hard to ever see a benchmark in which this particular effect makes Rust come out faster than any other language, because in the natural process of optimizing any particular benchmark for a non-Rust language, the benchmark will naturally not involve taking random copies "just in case", but this can have a significant impact on all that real code out in the real world not written for benchmarking. Casually written Rust can safely not make lots of copies, casually written code in almost anything else will probably have a lot more copying than the programmer realizes. [1]: https://blogs.oracle.com/javamagazine/post/records-come-to-java https://blogs.oracle.com/javamagazine/post/records-come-to-j...
- 1y ago
- cogman10 1y ago> so many of Google’s internal libraries make defensive copies in many places, This, IMO, is a sign of poor design. What are you trying to protect? That the google library isn't modifying something or that the caller of the google library isn't concurrently modifying something? Or are your storing off the value for later use? In any case, it's acceptable in the Javadoc and API to specify "If you give me this, you cannot further modify it". This already happens and is expected in common JDK data-structures. For example, if you put an element into a HashSet and then change the hash, you won't be able to find it again in the HashSet. Nobody complains that's the case because it's a "Well duh, you shouldn't have done that". Similarly, if you mutate a map while accessing it you'll get a "ConcurrentModificationException" or even bad results. Again, completely expected behavior. If you are worried about your code doing the wrong thing with something, then one defense that is easy to deploy is wrapping that object with one the is unmodifiable. That's why the JDK has the likes of `Collections.unmodifiableSet`. That doesn't do a defensive copy and is just a quick wrapper on the incoming set. Defensive programming has it's place. However, I think it gets over-deployed.
- lmm 1y agoRewriting in any language usually makes things much faster and better. That's an interesting anecdote but without a comparison with a C++ -> Java rewrite you can't really generalise.
- kccqzy 1y agoI had a similar experience rewriting a Go service in C++. Even when using a somewhat heavyweight Google framework (Scaffolding without Boq) even a simplistic rewrite can easily reach 1200qps per core. Over time I gradually optimized it into 8000qps per core through things like ditching fibers for async, tuning event managers, etc.