Maybe it's not so much runtime type checking, as the fact that a type of "any" means that the compiler doesn't know anything about what objects are in the deque. If you're using arrays to represent your deques, then this means you can't actually store the entries of the deque in the array, but only pointers to those entries, no? I think the keyword for what I'm saying is boxing/unboxing.
This is correct. When a function/struct accepts an interface value, it should be able to work with values of any underlying type. Underlying types can have vastly different sizes (from 0 to N bytes), and at the same time, we want to have a single machine code implementation per function, and a single memory layout per struct, known at compile time, to avoid massive code bloat (which we would have if we tried to account for all possible sizes). The simplest way to do it is to make an interface value store a pointer to actual data - pointers are always of the same size, no matter what the underlying type is. A pointer means "escaping to the heap", i.e. we can't safely store data on the stack anymore, because the pointer can be saved somewhere else and used later, when the original stack frame is long gone, which would be unsafe (we'd access dangling/overridden data).
So the main overhead is additional GC pressure because we have to allocate on the heap. Type checking has its overhead, too, but IIRC it's just a conditional branch.
Question for Go programmers: does using the `any` type result in an additional memory lookup if the wrapped type is a sized primitive like an int, or is the value stored directly alongside the type information?
[1] https://github.com/sekoyo/deque/blob/86df0003850acaf3039c2d6...
Edit: I wrote that before you added the second paragraph, which sounds right to me.
Hell the first benchmark just pushes a bunch of `int` onto the stack, with generics it just does that, without it has to pack the int into an interface{} quadrupling its size. The performance difference may well just come from the increase in allocations (and reallocation), and cache pressure, that sort of things. Nothing complicated, really.
> missed optimization in the compiler.
Go's compiler is pretty simplistic and not a heavily optimising one, so it'd also be unsurprising if type erasure also hindered it.
I don't know whether creating an interface{} has a runtime cost (though it may if the type pointer has to be looked up at runtime?), however there's also a memory overhead: I didn't look at everything but the first two benches (pushfront and pushbach) use `int` values, which are 32 bits. But interface{} is 2 words (128 bits).
This quadruples the memory requirements, meaning the backing buffer may well need either more reallocations, or to make larger allocations & have to span multiple cache lines.
As far as I understand, with interface{} the compiler can't just decide to generate static dispatch based code because there's not enough information to generate that in a generalisable way (the code can depend on information that is only available at run-time rather than compile-time) and it would be bad for programmers who expect dynamic dispatch to be used because they're optimising for something else e.g. binary size or compilation speed.
Obviously generics wouldn't be a performance improvement over using, say, code generation or doing a bunch of copy-pasting (and, given Go's lack of function overloading, a bunch of annoying renaming and some logic to dispatch to those methods which actually might cause more of a performance hit...) but those approaches tend to cause other problems.
You can have a look at the essembly code generated by Go program while using a slice of interface{} vs using slice of specific type on compiler explorer.
Edit: Removed C ... somehow have a habit of writing C/C++ together :)
It turns out that that complex technology was warranted?!?
Edit: Reminds me of this funny video https://youtu.be/-AQfQFcXac8?t=63
Not having generics isn't a pragmatic choice per se
I don't think many people would call Java 1.5 or C# 2.0 super complicated.
Do you know what Java 1.5 and C# 2.0 had in common, that Go didn't? Generics.
For reference:
- Java 1.5 - release date: September 30, 2004
- C# 2.0 - release date: January 22, 2006
That's ~18 years ago and 16 years ago, for whoever's keeping track.
By generics "on their own", I mean essentially the Hindley-Milner type system, which supports generics and can do static type inference in linear time: https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_sy...
Somehow, the "generics wars" in Go made generics into the scapegoat of programming language complexity, when perhaps it was Go's "interfaces" feature all along. Maybe the enemy was here the whole time!
That has never been the source of contention, and the benefits were noted from the initial discussions of the language before it reached an audience. Like all things it's a trade off. Will you be providing more value with generics, or are other features a higher priority. That's obviously a matter of perspective.
Go has many advantages over other languages, and strict typing, generics and raw speed has never been part of that. If those things are paramount for you, there are better languages suitable.
Go has been fantastic for the problem space I'm targeting. Fast enough, efficient enough, portable enough, simple enough that hiring is easy. This might not be true for what you work on, but I'm not writing a video decoder.
Hmm it's not known for that, is it? Go is known to be quite slow due to a weak compiler and a GC that optimizes for latency over throughput. And is Go really easier than a language like Java or in the dynamic world, JavaScript? It sure does seem to have a lot of weird edge cases and potholes to trip over.
Anyway I see a bunch of comments saying generics shouldn't make anything faster. They can make code go faster in combination with specialization and value types. It's one of the reasons C++ can go very fast whilst still supporting higher level abstractions, albeit at the cost of generated code size. It's also the reason they're adding value types and specialized generics to the JVM. I don't know if Go is doing C++/Rust style specialization or not, but it at least has the potential to do so.
It’s a hugely generalised statement so the real answer is “it really depends on your context”
Is it fast compared to other easy to learn languages? Definitely. Is it fast compared to other systems languages? Lol no. But is it fast to compile compared to other systems languages? Generally, yeah.
Is it easy to learn compared to other systems languages? Very much so. Is it easy to write complex low level systems logic? No. Is it easy to write dynamic code? No.
The problem with generalised statements against general purpose languages is that the scope is so wide that people will cherry whatever context they want to suit whatever argument (for or against) they choose.
I think this is debatable and depends on your background.
If you come from a Java/C background then many things are unintuitive e.g. error handling, dynamic interfaces, package/library management, whitespace parsing, style enforcement etc.
Golang is easy to learn right now because it's basic like say Java 1.2. But since it likes to ignore what other languages do I wouldn't say it's an inherently easy language.
Lol what ? Go is generally 'the' tool me and quite a few of my friends reach for when they want todo some heavy-lifting without getting bogged down in the weeds of things. Sure you millage may vary.
And in your view Go might be slow (compared to) ? But Go is definitely NOT slow for any sort of general computing. Faster than NodeJS, (ok i'm cherry picking now) mostly on par or with C# or Java depending on your benchmark-magic of choice. Super easy to do multicore and concurrency with it and definitely faster to compile than Rust.
Does it have warts or not the best language ? Absolutely ! But it's definitely not know to be "quite slow" and having a "weak compiler" ?? To be honest in this is the first time I've heard that exact claim "quite slow" :/
I understood it was comparable to Java or C#, both of which are extremely fast - both in terms of execution speed and more so in terms of developer productivity.
The benchmarks I’ve seen generally back that up
https://plummerssoftwarellc.github.io/PrimeView/report?id=da...
9th overall - faster than c++ for example in Dave Plummers primes benchmark. I quite like that benchmark because it’s really just a bunch of memory operations, a decent view of the compilers ability to generate decent code. Although my concern with this benchmark is that not all languages got the same optimisation effort. Looking at git commits, it seems like rust got almost more optimisation effort than every other language combined.
Also i don’t like the unfiltered view of that benchmark - lisp comes out fastest language of all but that’s because languages like c++ and Rust can only really pre calculate results at compile time (constexpr etc) - whereas that lisp implementation is doing everything before compilation. Unfair because other languages don’t have a programmable precompilation reader phase.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
In the area of java, Swift etc i don’t like these benchmarks at all though. The c# one for example often relies on c libraries or vector intrinsics etc so it’s not really like for like.
https://www.techempower.com/benchmarks/#section=data-r20&hw=...
Between java and c#
Except when it is: this C# program seems to have been transliterated from the Java program —
mandelbrot C# .NET #5
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
mandelbrot Java #2
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
And in the listing —
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
rust, c/c++, java etc. Compared to these languages, go is much easer to learn and use. Java can be put the side, the real comparison is with c/c++ and rust.
By taking some trade offs on the performance side, go has a much better UX than rust/c.. Literally open up a few tabs, go to github and compare a page of go code vs rust vs c/c++..
Generally speaking, you can expect a well-written Go program to be no more than 50% slower than well-written C. Normally the Go program will only be 5% or 10% slower.
Saying this another way, a carefully written C program that takes 100 seconds to run can be converted to Go and might take 150 seconds to run, in a bad case. In a typical situation the Go program will take 110 seconds to run.
Of course, a bad programmer can make anything slow.
1. A fast AOT compiler (which therefore, cannot spend much time optimizing things)
2. A low latency GC (which therefore, sacrifices throughput)
3. Ease of use (which therefore, means avoiding complicating the language with optimization related features)
Not included: raw performance.
I don't know how you're measuring "slow" but from where I sit in the software ecosystem Go is typically one of, if not _the_, fastest language available. It's entirely correct to optimize for latency over throughput, for my use cases.
Does it have a lot of weird edge cases? Probably yes. I'm no expert in Go, but I don't think it has more or less than any other language.
If you’ll excuse the blog spam, I did a dive into the performance of interface{} here: https://adrummond.net/posts/gogenerics
(The blog post was written before it was certain that Go would get generics.)
Which is.. 3x? or 10x?
> These tests are probably just picking up the cost of an extra allocation required to box a primitive type as an interface.
I've seen unboxing deliver 100x speed ups cause it allowed the compiler to auto-vectorize multiple loops, fuse them, optimize stuff better etc. Stuff that it can't do if it doesn't know the memory layout of things, or if there are "optimization barriers" inserted in between all these steps (`interface {}`).
---
TBH it's 2022 and it seems that "Go" devs are just seeing fire for the first time in their lives. Yeah, fire heats. And yeah, this type of stuff if why people don't put Go in the same bag of languages as C++ or Rust.
Go feels fast if you are coming from Python, but if you actually look at what the hardware can deliver in terms of FLOPs and BW, Go programs run at 0% of hardware peak (and no, just because your task manager shows go threads using 100% of the CPU doesn't mean that it is achieving 100% of peak CPU perf).
Boxing can be faster or slower depending on the context. For example, if the objects you are storing are large, unboxing them (rather than storing pointers to them) could well make the queue perform worse, as extending the arrays will require more memory to be copied.
There’s no doubt that the unboxing permitted by (Go’s) generics will sometimes lead to better performance. But I think that the article is exaggerating.
> TBH it's 2022 and it seems that "Go" devs are just seeing fire for the first time in their lives.
Come on, let’s keep this patronizing and flamey kind of commentary off HN. I for one am a language nerd who’s tried plenty of languages with various forms of generics. I’ve even been paid to write Haskell code! That said, I was pretty happy with Go without generics.
The difference between passing an unboxed generic and triggering monomorphization vs passing a boxed type-erased generic is night and day in terms of performance.\
Boxed generics generate one version of the code that need to work for all types, independently of layout, and dispatch via the generic ABI (aka `interface {}` in Go).
Unboxed generics generate one version of the code per type, removing memory allocations, and allowing the compiler to optimize the function for each type and each layout independently.
This increases code size and compilation times, but in many cases allows dozens of compiler optimizations that aren't possible for the boxed generic case. The code produced by these optimizations allow others to run, etc. And a 100x improvement in perf isn't uncommon (i've seen order of magnitude larger improvements and regressions in perf than that from switching back and forth between boxed and unboxed generics in Rust).
I think (but am not sure) that the Go compiler can add some small primitive types and structs (precisely: those that fit in a machine word) to a []interface{} by storing them inline, removing the need for an allocation. So perhaps you will only see major speed increases when using larger structs.
Unboxing is typically an optimization, for sure. I guess I disagree that it’s a ‘huge’ one except maybe in the case of arrays/slices (where you can do it anyway in Go without generics).
Historically the Go compiler has been seeing performance improvements every release, so historically, Go has always been getting faster.
When it comes to Generics, the benefits are not so straightforward as simply recompiling with a new binary. I just rebuilt one of our larger services with Go 1.17 and compared it to 1.18 and the benchmarks come out roughly the same after variance. There is a slight improvement, but what's interesting is that we don't use generics anywhere.
My employer has a blanket ban on Generics in Go until at least the next release. I know others that do too. There is also nothing in our code base that is screaming to be rewritten with Generics. We collectively thought about it and no one could come up with anything worthwhile. Internally, we're still not sure when they're even warranted. Sure, there's a few three line for-loops that could be removed, but none of those are in a hot path. Yawn.
If Go generics radically change the Go ecosystem overnight, they will ruin the language. The ecosystem prefers stability and compatibility over features, and it's pervasive and good. I update Java and JavaScript dependencies regularly and it's a fucking nightmare compared to Go.
Lastly, Go had attracted a large community of developers who eschew unnecessary abstractions. It's lovely and refreshing. I can't say the same about Rust or Swift or Scala, where complexity is sometimes celebrated as simplicity when it's really just convenience wearing lipstick.
Of course i never panicked or yelled about it nor really even bothered mentioning it (aside from on HN now and then, usually in Go posts since the whole "generics in go" remind me of that time :-P). But me not doing that doesn't mean i didn't think of it and i'm guessing others did too.
My guess is that nowadays you see more complaints about generics in Go because Go largely attracted a higher percentage of people who liked its simplicity than Java ever had (and more people in absolute numbers in general as i'd bet that there are way more programmers nowadays than in late 90s/early 2000s, so you're bound to hear more voices). After all in terms of features as a language Go didn't had anything to offer over Java whereas Java had a lot to offer back in the late 90s/early 2000s.
(amusingly the language i use most nowadays is Free Pascal which is a hodgepodge of arbitrary features and a far cry from the original Java simplicity - though i do often find annoying how much of a "mess" it is)
The problem is, no one can agree on what is the 'necessary level of abstraction'. Where you stand on that scale determines whether you love or hate Go as a language.
I do have some use for generics in my code: I've got a few conversion functions too many, but since they're narrowly typed, I don't expect a speed improvement, just a shorter file. Perhaps some of the (un)marshalling can be done more efficiently, too. Nothing worth losing any sleep over, so waiting another version sounds like a good strategy. I'll just play around with it a bit.
It's not. The Go 1 backwards compatibility promise means existing interfaces won't change. Take the Math package for instance: I'm not aware of any planned breaking changes -- if you are, please share. If they were to add support for non-int64 operations, they would likely be separate functions or a new package. This, without sweeping code changes, nothing changes.
Did you even read the article?
For example, BenchmarkPushFront-10 takes 9 ns... That should consist of one write to memory (to update the pointer in the object). With decent compiler optimizations and a modern out of order CPU, I think we ought to be expecting one of those per clock cycle, so we ought to be seeing performance of 0.5ns. Perhaps even faster if the compiler manages to vectorize stuff (although I'm not aware of any compiler that can vectorize memory allocations).
[0] https://blog.logrocket.com/benchmarking-golang-improve-funct...
1) The resizing seems worse to me than a linked-list of fixed-size ring buffers which use a sync.Pool. 2) (more out there) some of the time when I've implemented this sort of thing, it's been to allocate memory for unmarshaling. It might be nice to have a mechanism to store values and allocate pointers.
Real missed opportunity here