I'd argue that golang is inherently not a systems language, with its mandatory GC managed memory. I think it's a poor choice for anything performance or memory sensitive, especially a database. I know people would disagree (hence all the DBs written in golang these days, and Java before it), but I think C/C++/Rust/D are all superior for that kind of application.
All of which is to say, I don't think it matters. Use the right tool for the job - if you care about generic overhead, golang is not the right thing to use in the first place.
I have a slightly contrary opinion. Systems software is a very large umbrella, and much under that umbrella is not encumbered by a garbage collector whatsoever. (To add insult to injury, the term's definition isn't even broadly agreed upon, similar to the definition of a "high-level language".) Yes, there are some systems applications where a GC can be a hindrance in practice, but these days I'm not even sure it's a majority of systems software.
I think what's more important for the systems programmer is (1) the ability to inspect the low-level behavior of functions, like through their disassembly; (2) be reasonably confident how code will compile; and (3) have some dials and levers to control aspects of compiled code and memory usage. All of these things can and are present, not only in some garbage collected languages, but also garbage-collected languages with a dynamic type system!
Yes, there are environments so spartan and so precision-oriented that even a language's built-in allocator cannot be used (e.g., malloc), in which case using a GC'd language is going to be an unwinnable fight for control. But if you only need to do precision management of a memory that isn't pervasive in all of your allocation patterns, then using a language like C feels like throwing the baby out with the bath water. It's very rarely "all or nothing" in a modern, garbage-collected language.
You really haven't given any supporting information for your argument other than a vague feeling that GC is somehow bad. In fact you just pointed out many counterexamples to your own argument, so I'm not sure what to take away.
I've seen this sentiment a lot, and I never see specifics. "GC is bad for systems language" is an unsupported, tribalist, firmly-held belief that is unsupported by hard data.
On the other hand, huge, memory-intensive and garbage-collected systems have been deployed in vast numbers by thousands of different companies for decades, long before Go, within acceptable latency bounds. And shoddy, poorly performing systems have been written in C/C++ and failed spectacularly for all kinds of reasons.
I've said this before, and I'll say it again. People lump Go in with C/C++/Rust because they all (can) produce static binaries. I don't need to install Go's runtime like I install Java/NodeJS/Python runtimes. Honestly, I think it speaks so much to Go's accomplishments that it performs so well people intuitively categorize it with the systems languages rather than other managed languages.
Are you writing an application where Go's garbage collector will perform poorly relative to rolling your own memory management?
Maybe, those applications exist, but maybe not, it shouldn't be presumed.
I'm more open to the argument from definition, which might be what you mean by 'inherently': there isn't an RFC we can point to for interpreting what a systems language is, and it could be useful to have a consensus that manual memory management is a necessary property of anything we call a systems language.
No such consensus exists, and arguing that Go is a poor choice for $thing isn't a great way to establish whether it is or is not a systems language.
Go users certainly seem to think so, and it's not a molehill I wish to die upon.
this has been argued ad nauseum a decade ago and it boils down to your definition of 'systems'. at google scale, a system is a mesh of networked programs, not a kernel or low-level bit-banging tool.
It can be a right old bugger - I've been tweaking gron's memory usage down as a side project (e.g. 1M lines of my sample file into original gron uses 6G maxrss/33G peak, new tweaked uses 83M maxrss/80M peak) and there's a couple of pathological cases where the code seems to spend more time GCing than parsing, even with `runtime.GC()` forced every N lines. In C, I'd know where my memory was and what it was doing but even with things like pprof, I'm mostly in the dark with Go.
> I'd argue that golang is inherently not a systems language
First you'd have to establish what "systems" means. That, you'll find, is all over the place. Some people see systems as low level components like the kernel, others the userland that allows the user to operate the computer (the set of Unix utilizes, for example), you're suggesting databases and things like that.
The middle one, the small command line utilities that allow you to perform focused functions, is a reasonably decent fit for Go. This is one of the places it has really found a niche.
What's certain is that the Go team comes from a very different world to a lot of us. The definitions they use, across the board, are not congruent with what you'll often find elsewhere. Systems is one example that has drawn attention, but it doesn't end there. For example, what Go calls casting is the opposite of what some other languages call casting.
I agree. golang is not really a system programming. It's more like java, a language for applications.
It does have one niche, that it includes most if not everything you need run a network-based service(or micro-service), e.g http,even https, dns...are baked in. You no longer need to install openssl on windows for example, in golang one binary will include all of those(with CGO disabled too).
I do system programming in c and c++, maybe rust later when I have time to grasp that, there is no way for using Go there.
For network related applications, Go thus far is my favorite, nothing beat it, one binary has its all, can't be easier to upgrade in the field too.
i agree with you that garbage collected languages are bad for systems programming but it's not because garbage collection is inherently bad, it's because gc doesn't handle freeing resources other than memory. for better or worse i've spent most of my professional career writing databases in java and i will not start a database product in java or any other garbage collected language again. getting the error handling and resource cleanup right is way harder in java or go than c++ or rust because raii is the only sane way to do it.
Rust itself will most likely get some form of support for local, "pluggable" garbage collection in the near future. It's needed for managing general graphs with possible cycles, which might come up even in "systems programming" scenarios where performance is a focus - especially as the need for auto-managing large, complex systems increases.
I'd say Golang is a systems language if you consider the system to be interconnected services, sort of like a C for the HTTP and Kubernetes age. It has better performance than typical scripting languages, but it's probably not meant to write truly low-level code. I'd argue that GC doesn't matter on a typical networked service scale.
I would say Go is a systems programming language. A systems programming language is for creating services used by actual end user applications. That is pretty much what Go is being used for. Who is writing editors or drawing applications in Go? Nobody.
Go does contain many of the things of interest to systems programmers such as pointers and the ability to specify memory layout of data structures. You can make your own secondary allocators. In short it gives you far more fine grained control over how memory is used than something like Java or Python.
it seems there exists a category of Go programs for which escape analysis entirely obviates heap allocations, in which case if there is any garbage collection it originates in the statically linked runtime.
Honest question, what is golang a good choice for? It seems to inhabit the nether realm between high level productivity and low level performance, not being good at either.
We have been using golang for a long time without generic overhead. Why is now that it’s added we stop caring about performance. Is this the same HN that complains about memory bloat in every JavaScript thread?
Typical gatekeeping. I like Go, because it lets me get stuff done. You could say the same about JavaScript, but I think Go is better because of the type system. C, C++ and Rust are faster in many cases, but man are they awful to work with.
C and C++ dont really have package management to speak of, its basically "figure it out yourself". I tried Rust a couple of times, but the Result/Option paradigm basically forces you into this deeply nested code style that I hate.
FWIW you can have two different compilers (and outputs) for the same input: https://godbolt.org/z/bb1oG9TbP in the compiler pane just click "add new" and "clone compiler" (you can actually drag that button to immediately open the pane in the right layout instead of having your layout move to vertical thirds and needing to move the pane afterwards).
Learned that watching one of Matt's cppcon talks (A+, would do again), as you can expect this is useful to compare different versions of a compiler, or different compilers entirely, or different optimisation settings.
But wait, there's more! Using the top left Add dropdown, you can get a diff view between compilation outputs: https://godbolt.org/z/s3WxhEsKE (I maximised it because a diff view getting only a third of the layout is a bit narrow).
I feel like a half-idiot but I'm unable to tell how exactly the "unified=1" implements monomorphization. I don't see the extra indirections which OP writes about.
This is a really long and informative article, but I would propose a change to the title here, since "Generics can make your Go code slower" seems like the expected outcome, where the conclusion of the article leans more towards "Generics don't always make your code slower", as well as enumerating some good ways to use generics, as well as some anti-patterns.
Is it the expected outcome? I was under the initial impression that the author also noted:
> Overall, this may have been a bit of a disappointment to those who expected to use Generics as a powerful option to optimize Go code, as it is done in other systems languages.
where the implementation would smartly inline code and have performance no worse than doing so manually. I quite appreciated the call to attention that there's a nonobvious embedded footgun.
(As a side note, this design choice is quite interesting, and I appreciate the author diving into their breakdown and thoughts on it!)
Interestingly the original title and your proposed title imply, to me, the opposite of what I think they imply to you. This suggestion is really unclear.
For me Go has replaced Node as my preferred backend language. The reason is because of the power of static binaries, the confidence that the code I write today can still run ten years from now, and the performance.
The difference in the code I’m working with is being able to handle 250 req/s in node versus 50,000 req/s in Go without me doing any performance optimizations.
From my understanding Go was written with developer ergonomics first and performance is a lower priority. Generics undoubtedly make it a lot easier to write and maintain complex code. That may come at a performance cost but for the work I do even if it cuts the req/s in half I can always throw more servers at the problem.
Now if I was writing a database or something where performance is paramount I can understand where this can be a concern, it just isn’t for me.
I’d be very curious what orgs like CockroachDB and even K8s think about generics at the scale they’re using them.
One of the major pain points we have with Go is the lack of language support for monomorphization. We rely on a hand-built monomorphizing code generator [0] to compile CockroachDB's vectorized SQL engine [1]. Vectorized SQL is about producing efficient, type and operator specific code for each SQL operator. As such, we rely on true monomorphization to produce a performant SQL query engine.
I have a hope that, eventually, Go Generics will be performant enough to support this use case. As the author points out, there is nothing in the spec that prevents this potential future! That future is not yet here, but that's okay.
There are probably some less performance-sensitive use cases within CockroachDB's code base that could benefit from generics, but we haven't spend time looking into it yet.
Go was created with simplicity of feature set in mind, which does not translate into developer ergonomics automatically. It rather offers a least common denominator of lang features, so that most devs can handle it, who previously only handled other languages like Java and similar. This way Google aimed at attracting those devs. They'd not have to learn much to make the switch.
True developer ergonomics, as far as a programming language itself goes, stems from language features, which make goals easy to accomplish in little amount of code, in a readable way, using well crafted concepts of the language. Having to go to lengths, because your lang does not support programming language features (like generics in Go for a long time) is not developer ergonomics.
There is the aspect tooling for a language of course, but that has not necessarily to do with programmming language design. Same goes for standard library.
250 vs. 50000 req/s seems like a too big of a difference to me. Sure Go is faster than Node but Node is no slough either, you might want to dig in some deeper why you only got 250 req/s with Node.
> The difference in the code I’m working with is being able to handle 250 req/s in node versus 50,000 req/s in Go without me doing any performance optimizations.
Your node code should be in the 2k reqs/s range trivially, with many frameworks comfortable offering 5k+.
It is never going to be as fast as go, but it will handle most cases.
Really well written article. I liked that the author tried to keep a simple language around a fair amount of complex topics.
Although the article paints the Go solution for generics somewhat negative, it actually made me more positive to the Go solution.
I don't want generic code to be pushed everywhere in Go. I like Go to stay simple and it seems the choices the Go authors have made will discourage overuse of Generics. With interfaces you already avoid code duplication so why push generics? It is just a complication.
Now you can keep generics to the areas were Go didn't use to work so great.
Personally I quite like that Go is trying to find a niche somewhere between languages such as Python and C/C++. You get better performance than Python, but they are not seeking zero-overhead at any cost like C++ which dramatically increases complexity.
Given the huge amount of projects implemented with Java, C#, Python, Node etc there must be more than enough cases where Go has perfectly good performance. In the more extreme cases I suspect C++ and Rust are the better options.
Or if you do number crunching and more scientific stuff then Julia will actually outperform Go, despite being dynamically typed. Julia is a bit opposite of Go. Julia has generics (parameterized types) for performance rather than type safety.
In Julia you can create functions taking interface types and still get inlining and max performance. Just throwing it out there are many people seem to think that to achieve max performance you always need a complex statically typed language like C++/D/Rust. No you don't. There are also very high speed dynamic languages (well only Julia I guess at the moment. Possibly LuaJIT and Terra).
I expect we’re going to see most generic Go code happening at the lower levels of the stack. So cintainer libraries, utility/algo functions, and probably in some contexts around databases/ORMs. Outside of these contexts —- and because most usage will be able to simply leverage type deduction —- I’d guess most app code will look pretty similar to what we’ve seen before.
I'm excited about generics that gives you a tradeoff between monomorphization and "everything is a pointer". The "everything is a pointer" approach, like Haskell, is incredibly inefficient wrt execution time and memory usage, the "monomorphize everything" approach can explode your code size surprisingly fast.
I wouldn't be surprised if we get some control over monomorphization down the line, but if Go started with the monomorphization approach, it would be impossible to back out of it because it would cause performance regressions. Starting with the shape stenciling approach means that introducing monomorphization later can give you a performance improvement.
I'm not trying to predict whether we'll get monomorphization at some future point in Go, but I'm just saying that at least the door is open.
> "monomorphize everything" approach can explode your code size surprisingly fast.
It can in the naive implementation. Early C++ was famous for code bloat and (apparently) hasn't shaken that outdated impression.
In practice, monomorphization of templates hasn't been a serious issue in C++ for a long time. The compiler and linker technologies have advanced significantly.
Yes, they seem to have shipped a MVP first, which is a sensible approach. Controlling the extent of monomorphization requires changes in how the code is written, so if they had offered that exclusively it would've been a pitfall to existing users. By boxing everything, they keep their MVP closer to the previously idiomatic interface{} pattern.
Why is a speed part of the Go language contract but footprint of the executable is not? I, for one, would be quite miffed if an update of the Go compiler would mean an application would no longer fit on my mcu. That is worse then the application running slower.
My first use of Go generics has been for a concurrent "ECS" game engine. In this case, the gains are pretty obvious. I think.
I get to write one set of generic methods and data structures that operate over arbitrary "Component" structs, and I can allocate all my components of a particular type contiguously on the heap, then iterate over them with arbitrary, type-safe functions.
I can't fathom that doing this via a Component interface would be even as close as fast, because it would destroy cache performance by introducing a bunch of Interface tuples and pointer dereferencing for every single instance. Not to mention the type-unsafe code being yucky. Am I wrong?
FWIW I was able to update 2,000,000 components per (1/60s) frame per thread in a simple Game of Life prototype, which I am quite happy with. But I never bothered to evaluate if Interfaces would be as fast
Assuming your generic functions take _pointers_ to Components as input, full monomorphization does not occur and you're suffering a performance hit similar in magnitude, if not strictly greater empirically, to interface "dereferences".
On this basis, I don't believe your generic implementation is as faster than an interface implementation as you claim.
Sweet! I've been using it for the same. Example game project (did it for a game jam): https://github.com/nikki93/raylib-5k -- in this case the Go gets transpiled to C++ and runs as WebAssembly too. Readme includes a link to play the game in the browser. game.gx.go and behaviors.gx.go kind of show the ECS style.
It's worked with 60fps performance on a naive N^2 collision algo over about 4200 entities -- but also I tend to use a broadphase for collisions in actual games (there's an "externs" system to call to other C/C++ and I use Chipmunk's broadphase).
Great article, just skimmed it, but will definitely dive deeper into it. I thought Go is doing full monomorphization.
As another datapoint I can add that I tried to replace the interface{}-based btree that I use as the main workhorse for grouping in OctoSQL[0] with a generic one, and got around 5% of a speedup out of it in terms of records per second.
That said, compiling with Go 1.18 vs Go 1.17 got me a 10-15% speedup by itself.
> That said, compiling with Go 1.18 vs Go 1.17 got me a 10-15% speedup by itself.
Where did you see this speedup? Other than `GOAMD64` there wasn't much in the release notes about compiler or stdlib performance improvements so I didn't rush to get 1.18-compiled binaries deployed, but maybe I should...
(I do expect some nice speedups from using Cut and AvailableBuffer in a few places, but not without some rewrites.)
Hey, author here. Thanks for the kind words! This is a custom pipeline that I designed for the article. It's implemented as a Node.js library using SVG.js and it statically generates the interactive SVGs directly in the static site generator I was using (Eleventy) by calling out to the Go compiler and extracting assembly for any lines you mark as interesting. It turned out very handy for iterating, but it's not particularly reusable I'm afraid!
what I expect to happen now that golang has generics and reports like these will show up is golang will explore monomorphizing generics and get hard numbers. they may also choose to use some of the compilation speeds they've gained from linker optimizations and spend that on generics.
I can't imagine monomorphizing being that big of a deal during compilation if the generation is defered and results are cached.
I am unfamiliar with Go. This article discusses that they have decided to go for runtime lookup. Is there any reason why that implementation might make monomorphizing more difficult?
This is a great article yet with an unnecessarily sensationalist headline. Generics can be improved in performance over time, but a superstition like "generics are slow" (not the exact headline, but what it implies to reader) can remain stuck in our heads forever. I can see developers stick to the dogma of "never use generics if you want fast code", and resorting to terrible duplication, and more bugs.
> Ah well. Overall, this may have been a bit of a disappointment to those who expected to use Generics as a powerful option to optimize Go code, as it is done in other systems languages. We have learned (I hope!) a lot of interesting details about the way the Go compiler deals with Generics. Unfortunately, we have also learned that the implementation shipped in 1.18, more often than not, makes Generic code slower than whatever it was replacing. But as we’ve seen in several examples, it needn’t be this way. Regardless of whether we consider Go as a “systems-oriented” language, it feels like runtime dictionaries was not the right technical implementation choice for a compiled language at all. Despite the low complexity of the Go compiler, it’s clear and measurable that its generated code has been steadily getting better on every release since 1.0, with very few regressions, up until now.
And remember:
> DO NOT despair and/or weep profusely, as there is no technical limitation in the language design for Go Generics that prevents an (eventual) implementation that uses monomorphization more aggressively to inline or de-virtualize method calls.
I agree. I find this snippet interestingly incorrect.
> with very few regressions, up until now.
the idea that this is a regression is silly. you can't have a regression unless old code is slower as a result. which is clearly not the case. its just a less than ideal outcome for generics. which will likely get resolved.
> Inlining code is great. Monomorphization is a total win for systems programming languages: it is, essentially, the only form of polymorphism that has zero runtime overhead
Blowing your icache can result in slowdowns. In many cases it's worth having smaller code even if it's a bit slower when microbenchmarked cache-hot, to avoid evicting other frequently used code from the cache in the real system.
The essay is missing a "usually", but it's true that monomorphisation is a gain in the vast majority of situations because of the data locality and optimisation opportunities offered by all the calls being static. Though obviously that assumes a pretty heavy optimisation pipeline (so languages like C++ or Rust benefit a lot more than a language with a lighter AOT optimisation pipeline like Java).
Much as with JITs (though probably with higher thresholds), issues occur for megamorphic callsites (when a generic function has a ton of instances), but that should be possible to dump for visibility, and there are common and pretty easy solutions for at least some cases e.g. trampolining through a small generic function (which will almost certainly be inlined) to one that's already monomorphic is pretty common when the generic bits are mostly a few conversions at the head of the function (this sort of trampolining is common in Rust, where "conversion" generics are often used for convenience purposes so e.g. a function will take an `T: AsRef<str>` so the caller doesn't have to extract an `&str` themselves).
Monomorphisation is a double-edged blade. Sometimes keeping the code smaller and hot is better than inlining everything, especially when your application does not exclusively own all the system resources (an assumption that many “systems programming languages” sadly do). There is too much focus on “performance” aka. microbenchmarks, but they don’t tell you the whole story. If you have a heavily async environment, with multiple tasks running in parallel and waiting on each other in complex patterns, more compact, reusable code can not only speed up the work but also allow you to do more work per watt of energy.
I think it’s great that golang designers decided to follow Swift’s approach instead of specializing everything. The performance issues can be fixed in time with more tools (like monomorphissation directives) and profile-guided optimization.
This is a very interesting article. I was however a bit confused by the lingo, calling everything generics. As I understood it the main point of the article quite precisely matched the distinction between generics and templates as I learned it. Therefore what surprised me most was the fact that go monomorphizes generic code sometimes. Which however makes sense given the way go's module-system works – i.e. imported modules are included in the compilation – but doesn't fit my general understanding of generics.
Rust also enthusiastically monomorphizes generic code. Templates vs generics seems to be more about the duck typing C++ templates use vs generics doing type parameters with statically checked constraints on the types.
Similar to how the GC has become faster and faster with each version, we can expect the generics implementation to be too. I wouldn’t pay much attention to conclusions about performance from the initial release of the feature. The Go team is quiet open with their approach.
> there’s no incentive to convert a pure function that takes an interface to use Generics in 1.18.
Good. I saw a lot of people suggesting in late 2021 that you could use generics as some kind of `#pragma force-devirtualization`, and that would be awful if it became common.
Related. The introduction of Generics in Go revived an issue about the ergonomics of typesafe Context in a Go HTTP framework called Gin:
https://github.com/gin-gonic/gin/issues/1123
Meh. The people who screamed loudest about Generics missing in Go aren't going to be using the language now that the language has them, and are going to find something new to complain about.
The language will suffer now with additional developmental and other overhead.
If the information in this article is make-or-break for your program, you probably shouldn't have chosen Go.
In the grand space of all programming languages, Go is fast. In the space of compiled programming languages, it's on the slower end. If you're in a "counting CPU ops" situation it's not a good choice.
There is an intermediate space in which one is optimizing a particular tight loop, certainly, I've been there, and this can be nice to know. But if it's beyond "nice to know", you have a problem.
I don't know what you're doing with reflection but the odds are that it's wildly slower than anything in that article though, because of how it works. Reflection is basically like a dynamically-typed programming language runtime you can use as a library in Go, and does the same thing dynamically-typed languages (modulo JIT) do on their insides, which is essentially deal with everything through an extra layer of indirection. Not just a function call here or there... everything. Reading a field. Writing a field. Calling a function, etc. Everywhere you have runtime dynamic behavior, the need to check for a lot of things to be true, and everything operating through extra layers of pointers and table structs. Where the article is complaining about an extra CPU instruction here and an extra pointer indirection there, you've signed up for extra function calls and pointer indirections by the dozens. If you can convert reflection to generics it will almost certainly be a big win.
(But if you cared about performance you were probably also better off with an interface that didn't fully express what you meant and some extra type switches.)
Very few people are actually answering your question, so I'll answer it: Generics are slower than concrete types, and are slower than simple interfaces. However, the article does not bother to compare generics with reflection, and my intuition says that generics will be faster than reflection.
If you're using reflection or storing a bare interface{}, you should probably instead try using generics.
If you're using real interfaces, you should keep using interfaces.
If you care about performance, you should not try to write Java-Streams / FP-like code in a language with no JIT and a non-generational non-compacting GC.
Definitely not. In the general case, you will make things simpler and faster by turning reflection-based code into generic code.
What this article says is that a function that is generic on an interface introduces a tiny bit of reflection (as little as is necessary to figure out if a type conforms to an interface and get an itab out of it), and that tiny bit of reflection is quite expensive. This means two things.
One, if you're not in a position where you're worried about what does or does not get devirtualized and inlined, this isn't a problem for you. If you're using reflection at all, this definitely doesn't apply to you.
Two, reflection is crazy expensive, and the whole point of the article is that the introduction of that tiny bit of reflection can make function calls literally twice as slow. If you are in a position where you care about the performance of function calls, you're never really going to improve upon the situation by piling on even more reflection.
(off-topic) Anyone else using Firefox know why the text starts out light gray and then flashes to unreadably dark gray after the page loads? (The header logo and text change from gray to blue too)
The article articulates why it's reasonable to expect that generics would make Go faster. From TFA:
> Monomorphization is a total win for systems programming languages: it is, essentially, the only form of polymorphism that has zero runtime overhead, and often it has negative performance overhead. It makes generic code faster.
I'd expect without monomorphization the code should perform the same as interface{} code, perhaps minus type cast error handling overhead. That's the model where generics are passing interface{} underneath, & exist only as a type check (à la Java type erasure)
Yes? We used code generators to monomorphize our code in like 2015 and it was faster than using interfaces. Generics could reasonably produce the same code we did in 2015, but they don't.
Is there any large project that done an in-place replacement to use generics that has been benchmarked? I doubt that the change is even measurable in general.
Well sure. Not writing hand tuned assembly can make your code slower, too. Go's value as a language is how it fills the niche between Rust and Python, giving you low level things like manual memory control, while still making tradeoffs for performance and developer experience.
Some comments were deferred for faster rendering.
nvarsj|3 years ago
All of which is to say, I don't think it matters. Use the right tool for the job - if you care about generic overhead, golang is not the right thing to use in the first place.
reikonomusha|3 years ago
I think what's more important for the systems programmer is (1) the ability to inspect the low-level behavior of functions, like through their disassembly; (2) be reasonably confident how code will compile; and (3) have some dials and levers to control aspects of compiled code and memory usage. All of these things can and are present, not only in some garbage collected languages, but also garbage-collected languages with a dynamic type system!
Yes, there are environments so spartan and so precision-oriented that even a language's built-in allocator cannot be used (e.g., malloc), in which case using a GC'd language is going to be an unwinnable fight for control. But if you only need to do precision management of a memory that isn't pervasive in all of your allocation patterns, then using a language like C feels like throwing the baby out with the bath water. It's very rarely "all or nothing" in a modern, garbage-collected language.
titzer|3 years ago
I've seen this sentiment a lot, and I never see specifics. "GC is bad for systems language" is an unsupported, tribalist, firmly-held belief that is unsupported by hard data.
On the other hand, huge, memory-intensive and garbage-collected systems have been deployed in vast numbers by thousands of different companies for decades, long before Go, within acceptable latency bounds. And shoddy, poorly performing systems have been written in C/C++ and failed spectacularly for all kinds of reasons.
DoctorOW|3 years ago
samatman|3 years ago
Are you writing an application where Go's garbage collector will perform poorly relative to rolling your own memory management?
Maybe, those applications exist, but maybe not, it shouldn't be presumed.
I'm more open to the argument from definition, which might be what you mean by 'inherently': there isn't an RFC we can point to for interpreting what a systems language is, and it could be useful to have a consensus that manual memory management is a necessary property of anything we call a systems language.
No such consensus exists, and arguing that Go is a poor choice for $thing isn't a great way to establish whether it is or is not a systems language.
Go users certainly seem to think so, and it's not a molehill I wish to die upon.
baq|3 years ago
zimpenfish|3 years ago
It can be a right old bugger - I've been tweaking gron's memory usage down as a side project (e.g. 1M lines of my sample file into original gron uses 6G maxrss/33G peak, new tweaked uses 83M maxrss/80M peak) and there's a couple of pathological cases where the code seems to spend more time GCing than parsing, even with `runtime.GC()` forced every N lines. In C, I'd know where my memory was and what it was doing but even with things like pprof, I'm mostly in the dark with Go.
randomdata|3 years ago
First you'd have to establish what "systems" means. That, you'll find, is all over the place. Some people see systems as low level components like the kernel, others the userland that allows the user to operate the computer (the set of Unix utilizes, for example), you're suggesting databases and things like that.
The middle one, the small command line utilities that allow you to perform focused functions, is a reasonably decent fit for Go. This is one of the places it has really found a niche.
What's certain is that the Go team comes from a very different world to a lot of us. The definitions they use, across the board, are not congruent with what you'll often find elsewhere. Systems is one example that has drawn attention, but it doesn't end there. For example, what Go calls casting is the opposite of what some other languages call casting.
synergy20|3 years ago
It does have one niche, that it includes most if not everything you need run a network-based service(or micro-service), e.g http,even https, dns...are baked in. You no longer need to install openssl on windows for example, in golang one binary will include all of those(with CGO disabled too).
I do system programming in c and c++, maybe rust later when I have time to grasp that, there is no way for using Go there.
For network related applications, Go thus far is my favorite, nothing beat it, one binary has its all, can't be easier to upgrade in the field too.
jeffffff|3 years ago
zozbot234|3 years ago
wvh|3 years ago
JamesBarney|3 years ago
renewiltord|3 years ago
socialdemocrat|3 years ago
Go does contain many of the things of interest to systems programmers such as pointers and the ability to specify memory layout of data structures. You can make your own secondary allocators. In short it gives you far more fine grained control over how memory is used than something like Java or Python.
https://erik-engheim.medium.com/is-go-a-systems-programming-...
bborud|3 years ago
jjtheblunt|3 years ago
is that factual, in the general case?
it seems there exists a category of Go programs for which escape analysis entirely obviates heap allocations, in which case if there is any garbage collection it originates in the statically linked runtime.
blindmute|3 years ago
bogota|3 years ago
IgorPartola|3 years ago
That is a huge leap you are making there that I don’t think is exactly justified.
alephnan|3 years ago
Depends on what you're measuring as performance.
Server request throughput? Being middleware API server to relay data between a frontend and a backend?
svnpenn|3 years ago
C and C++ dont really have package management to speak of, its basically "figure it out yourself". I tried Rust a couple of times, but the Result/Option paradigm basically forces you into this deeply nested code style that I hate.
jksmith|3 years ago
water-your-self|3 years ago
jokoon|3 years ago
There are just not enough statically typed languages that don't use a GC.
mhh__|3 years ago
xyproto|3 years ago
Thaxll|3 years ago
komuW|3 years ago
Look at the assembly difference between this two examples:
1. https://godbolt.org/z/7r84jd7Ya (without monomorphization)
2. https://godbolt.org/z/5Ecr133dz (with monomorphization)
If you don't want to use godbolt, run the command `go tool compile '-d=unified=1' -p . -S main.go`
I guess that the flag is not documented because the Go team has not committed themselves to whichever implementation.
masklinn|3 years ago
Learned that watching one of Matt's cppcon talks (A+, would do again), as you can expect this is useful to compare different versions of a compiler, or different compilers entirely, or different optimisation settings.
But wait, there's more! Using the top left Add dropdown, you can get a diff view between compilation outputs: https://godbolt.org/z/s3WxhEsKE (I maximised it because a diff view getting only a third of the layout is a bit narrow).
kubanczyk|3 years ago
trey-jones|3 years ago
addcninblue|3 years ago
> Overall, this may have been a bit of a disappointment to those who expected to use Generics as a powerful option to optimize Go code, as it is done in other systems languages.
where the implementation would smartly inline code and have performance no worse than doing so manually. I quite appreciated the call to attention that there's a nonobvious embedded footgun.
(As a side note, this design choice is quite interesting, and I appreciate the author diving into their breakdown and thoughts on it!)
SomeCallMeTim|3 years ago
So no, generics do not de facto make code slower.
Ensorceled|3 years ago
unknown|3 years ago
[deleted]
zachruss92|3 years ago
The difference in the code I’m working with is being able to handle 250 req/s in node versus 50,000 req/s in Go without me doing any performance optimizations.
From my understanding Go was written with developer ergonomics first and performance is a lower priority. Generics undoubtedly make it a lot easier to write and maintain complex code. That may come at a performance cost but for the work I do even if it cuts the req/s in half I can always throw more servers at the problem.
Now if I was writing a database or something where performance is paramount I can understand where this can be a concern, it just isn’t for me.
I’d be very curious what orgs like CockroachDB and even K8s think about generics at the scale they’re using them.
jordanlewis|3 years ago
One of the major pain points we have with Go is the lack of language support for monomorphization. We rely on a hand-built monomorphizing code generator [0] to compile CockroachDB's vectorized SQL engine [1]. Vectorized SQL is about producing efficient, type and operator specific code for each SQL operator. As such, we rely on true monomorphization to produce a performant SQL query engine.
I have a hope that, eventually, Go Generics will be performant enough to support this use case. As the author points out, there is nothing in the spec that prevents this potential future! That future is not yet here, but that's okay.
There are probably some less performance-sensitive use cases within CockroachDB's code base that could benefit from generics, but we haven't spend time looking into it yet.
[0]: https://github.com/cockroachdb/cockroach/blob/master/pkg/sql...
[1]: https://www.cockroachlabs.com/blog/how-we-built-a-vectorized...
zelphirkalt|3 years ago
True developer ergonomics, as far as a programming language itself goes, stems from language features, which make goals easy to accomplish in little amount of code, in a readable way, using well crafted concepts of the language. Having to go to lengths, because your lang does not support programming language features (like generics in Go for a long time) is not developer ergonomics.
There is the aspect tooling for a language of course, but that has not necessarily to do with programmming language design. Same goes for standard library.
RedShift1|3 years ago
vorpalhex|3 years ago
Your node code should be in the 2k reqs/s range trivially, with many frameworks comfortable offering 5k+.
It is never going to be as fast as go, but it will handle most cases.
socialdemocrat|3 years ago
Although the article paints the Go solution for generics somewhat negative, it actually made me more positive to the Go solution.
I don't want generic code to be pushed everywhere in Go. I like Go to stay simple and it seems the choices the Go authors have made will discourage overuse of Generics. With interfaces you already avoid code duplication so why push generics? It is just a complication.
Now you can keep generics to the areas were Go didn't use to work so great.
Personally I quite like that Go is trying to find a niche somewhere between languages such as Python and C/C++. You get better performance than Python, but they are not seeking zero-overhead at any cost like C++ which dramatically increases complexity.
Given the huge amount of projects implemented with Java, C#, Python, Node etc there must be more than enough cases where Go has perfectly good performance. In the more extreme cases I suspect C++ and Rust are the better options.
Or if you do number crunching and more scientific stuff then Julia will actually outperform Go, despite being dynamically typed. Julia is a bit opposite of Go. Julia has generics (parameterized types) for performance rather than type safety.
In Julia you can create functions taking interface types and still get inlining and max performance. Just throwing it out there are many people seem to think that to achieve max performance you always need a complex statically typed language like C++/D/Rust. No you don't. There are also very high speed dynamic languages (well only Julia I guess at the moment. Possibly LuaJIT and Terra).
ovao|3 years ago
klodolph|3 years ago
I wouldn't be surprised if we get some control over monomorphization down the line, but if Go started with the monomorphization approach, it would be impossible to back out of it because it would cause performance regressions. Starting with the shape stenciling approach means that introducing monomorphization later can give you a performance improvement.
I'm not trying to predict whether we'll get monomorphization at some future point in Go, but I'm just saying that at least the door is open.
SomeCallMeTim|3 years ago
It can in the naive implementation. Early C++ was famous for code bloat and (apparently) hasn't shaken that outdated impression.
In practice, monomorphization of templates hasn't been a serious issue in C++ for a long time. The compiler and linker technologies have advanced significantly.
tines|3 years ago
zozbot234|3 years ago
qznc|3 years ago
rowanG077|3 years ago
pphysch|3 years ago
I get to write one set of generic methods and data structures that operate over arbitrary "Component" structs, and I can allocate all my components of a particular type contiguously on the heap, then iterate over them with arbitrary, type-safe functions.
I can't fathom that doing this via a Component interface would be even as close as fast, because it would destroy cache performance by introducing a bunch of Interface tuples and pointer dereferencing for every single instance. Not to mention the type-unsafe code being yucky. Am I wrong?
FWIW I was able to update 2,000,000 components per (1/60s) frame per thread in a simple Game of Life prototype, which I am quite happy with. But I never bothered to evaluate if Interfaces would be as fast
siftrics|3 years ago
On this basis, I don't believe your generic implementation is as faster than an interface implementation as you claim.
nikki93|3 years ago
It's worked with 60fps performance on a naive N^2 collision algo over about 4200 entities -- but also I tend to use a broadphase for collisions in actual games (there's an "externs" system to call to other C/C++ and I use Chipmunk's broadphase).
folago|3 years ago
brundolf|3 years ago
nunez|3 years ago
cube2222|3 years ago
As another datapoint I can add that I tried to replace the interface{}-based btree that I use as the main workhorse for grouping in OctoSQL[0] with a generic one, and got around 5% of a speedup out of it in terms of records per second.
That said, compiling with Go 1.18 vs Go 1.17 got me a 10-15% speedup by itself.
[0]:https://github.com/cube2222/octosql
morelisp|3 years ago
Where did you see this speedup? Other than `GOAMD64` there wasn't much in the release notes about compiler or stdlib performance improvements so I didn't rush to get 1.18-compiled binaries deployed, but maybe I should...
(I do expect some nice speedups from using Cut and AvailableBuffer in a few places, but not without some rewrites.)
eliben|3 years ago
throwoutway|3 years ago
Is there an open source CSS library or something that does this?
tanoku|3 years ago
jatone|3 years ago
I can't imagine monomorphizing being that big of a deal during compilation if the generation is defered and results are cached.
whimsicalism|3 years ago
sedatk|3 years ago
eatonphil|3 years ago
> Ah well. Overall, this may have been a bit of a disappointment to those who expected to use Generics as a powerful option to optimize Go code, as it is done in other systems languages. We have learned (I hope!) a lot of interesting details about the way the Go compiler deals with Generics. Unfortunately, we have also learned that the implementation shipped in 1.18, more often than not, makes Generic code slower than whatever it was replacing. But as we’ve seen in several examples, it needn’t be this way. Regardless of whether we consider Go as a “systems-oriented” language, it feels like runtime dictionaries was not the right technical implementation choice for a compiled language at all. Despite the low complexity of the Go compiler, it’s clear and measurable that its generated code has been steadily getting better on every release since 1.0, with very few regressions, up until now.
And remember:
> DO NOT despair and/or weep profusely, as there is no technical limitation in the language design for Go Generics that prevents an (eventual) implementation that uses monomorphization more aggressively to inline or de-virtualize method calls.
jatone|3 years ago
> with very few regressions, up until now.
the idea that this is a regression is silly. you can't have a regression unless old code is slower as a result. which is clearly not the case. its just a less than ideal outcome for generics. which will likely get resolved.
fulafel|3 years ago
Blowing your icache can result in slowdowns. In many cases it's worth having smaller code even if it's a bit slower when microbenchmarked cache-hot, to avoid evicting other frequently used code from the cache in the real system.
masklinn|3 years ago
Much as with JITs (though probably with higher thresholds), issues occur for megamorphic callsites (when a generic function has a ton of instances), but that should be possible to dump for visibility, and there are common and pretty easy solutions for at least some cases e.g. trampolining through a small generic function (which will almost certainly be inlined) to one that's already monomorphic is pretty common when the generic bits are mostly a few conversions at the head of the function (this sort of trampolining is common in Rust, where "conversion" generics are often used for convenience purposes so e.g. a function will take an `T: AsRef<str>` so the caller doesn't have to extract an `&str` themselves).
ribit|3 years ago
I think it’s great that golang designers decided to follow Swift’s approach instead of specializing everything. The performance issues can be fixed in time with more tools (like monomorphissation directives) and profile-guided optimization.
dse1982|3 years ago
ben0x539|3 years ago
maxekman|3 years ago
morelisp|3 years ago
Good. I saw a lot of people suggesting in late 2021 that you could use generics as some kind of `#pragma force-devirtualization`, and that would be awful if it became common.
mcronce|3 years ago
YesThatTom2|3 years ago
They care about making excuses about not using Go.
ctvo|3 years ago
hu3|3 years ago
If anyone can contribute, please do.
slackfan|3 years ago
The language will suffer now with additional developmental and other overhead.
The world will continue turning.
kubb|3 years ago
jerf|3 years ago
In the grand space of all programming languages, Go is fast. In the space of compiled programming languages, it's on the slower end. If you're in a "counting CPU ops" situation it's not a good choice.
There is an intermediate space in which one is optimizing a particular tight loop, certainly, I've been there, and this can be nice to know. But if it's beyond "nice to know", you have a problem.
I don't know what you're doing with reflection but the odds are that it's wildly slower than anything in that article though, because of how it works. Reflection is basically like a dynamically-typed programming language runtime you can use as a library in Go, and does the same thing dynamically-typed languages (modulo JIT) do on their insides, which is essentially deal with everything through an extra layer of indirection. Not just a function call here or there... everything. Reading a field. Writing a field. Calling a function, etc. Everywhere you have runtime dynamic behavior, the need to check for a lot of things to be true, and everything operating through extra layers of pointers and table structs. Where the article is complaining about an extra CPU instruction here and an extra pointer indirection there, you've signed up for extra function calls and pointer indirections by the dozens. If you can convert reflection to generics it will almost certainly be a big win.
(But if you cared about performance you were probably also better off with an interface that didn't fully express what you meant and some extra type switches.)
lalaithion|3 years ago
LanceH|3 years ago
AYBABTME|3 years ago
morelisp|3 years ago
If you're using real interfaces, you should keep using interfaces.
If you care about performance, you should not try to write Java-Streams / FP-like code in a language with no JIT and a non-generational non-compacting GC.
pdpi|3 years ago
What this article says is that a function that is generic on an interface introduces a tiny bit of reflection (as little as is necessary to figure out if a type conforms to an interface and get an itab out of it), and that tiny bit of reflection is quite expensive. This means two things.
One, if you're not in a position where you're worried about what does or does not get devirtualized and inlined, this isn't a problem for you. If you're using reflection at all, this definitely doesn't apply to you.
Two, reflection is crazy expensive, and the whole point of the article is that the introduction of that tiny bit of reflection can make function calls literally twice as slow. If you are in a position where you care about the performance of function calls, you're never really going to improve upon the situation by piling on even more reflection.
linkdd|3 years ago
Just implement naively, then if you have performance issues identify the bottleneck.
zellyn|3 years ago
wtetzner|3 years ago
torginus|3 years ago
jimmaswell|3 years ago
Couldn't JIT do this?
kevwil|3 years ago
throwaway894345|3 years ago
> Monomorphization is a total win for systems programming languages: it is, essentially, the only form of polymorphism that has zero runtime overhead, and often it has negative performance overhead. It makes generic code faster.
__s|3 years ago
I'd expect without monomorphization the code should perform the same as interface{} code, perhaps minus type cast error handling overhead. That's the model where generics are passing interface{} underneath, & exist only as a type check (à la Java type erasure)
anonymoushn|3 years ago
lokar|3 years ago
unknown|3 years ago
[deleted]
unknown|3 years ago
[deleted]
AtNightWeCode|3 years ago
dmullis|3 years ago
[deleted]
JaggerFoo|3 years ago
[deleted]
throwaway894345|3 years ago
ki_|3 years ago
ramesh31|3 years ago
mrweasel|3 years ago