Less is Exponentially More (2012)
commandcenter.blogspot.com
commandcenter.blogspot.com
Go is obviously a superior language to C++ for writing concurrent programs in every way except possibly when you need to achieve maximum possible throughput (as in google's search clusters) or minimum possible latency (are HFT shops switching to go? I honestly don't know). However, outside of a few companies that have huge capacity problems, people weren't writing concurrent request serving programs in C++. They were using Python, Java, etc., and later, Node.js. So its no surprise that people flocked to Go from those languages and not C++, because there wasn't a huge population of C++ programmers in this problem domain looking for a better language in the first place.
The other C++ programmers were down the hall, working on Chrome and Android (which has a ton of C/C++, despite Java being the primary app development language). The reason they didn't, can't, and won't switch is that managed languages like Go are not well suited to highly resource constrained environments like phones, or browsers that are supposed to run on win XP machines. (To say nothing of games).
The advantage of C++ is not, as Pike pompously suggests, that C++ programmers have too much fun playing around with templates and other navel-gazing pursuits. The advantage is that it is a high-level language that lets you do what you need to do with very, very close to the minimum possible resource usage. And there are programming environments where that really matters, and those environments are never going away.
Points often cited as disadvantages of references include:
- There's nothing that clearly distinguishes them from regular variables at the point of use. They can therefore obscure the fact that you're not passing by value or mutating a local/member variable (particularly if you don't have good IDE support, and good IDE support for C++ is damn hard for some environments/toolchains/targets)
- There is no concept of a null reference, which can create friction with interfaces that use NULL for optional parameters
These points are also often cited as crucial advantages of references. So it goes.
The first can be a legitimate complaint, although I find it's rarely an issue in practice.
If you need the equivalent of a nullable reference, you can just use std::optional<std::reference_wrapper<T>> instead and make the nullability explicit.
U do_something(const std::vector<T>& vec); output_type do_something(buffer_view input); U do_something(std::vector<T> const *vec) {
T item = (*vec)[1];
...;
}
We can get away with this because the type `std::vector<T>` is known at compile time. It goes back to C and passing pointers of structs all over the place.The const on the right side makes sure the compiler keeps the memory at the pointed location safe like the const used in your initial question.
Go was specifically designed to be suitable for programs like web browsers, compilers, IDEs, maybe even OSes. We know because Pike said so - see the "Systems Language" page at https://web.stanford.edu/class/ee380/Abstracts/100428-pike-s... (warning, PDF).
In particular, I think the idea that anyone serious is going to build a large desktop application like a web browser in a language that doesn't expose OS-level threads to the programmer is pretty laughable. Similarly for the idea of building an OS (or at least an OS kernel) in a garbage collected language.
edit: I realize I didn't really respond to your point. My point is that regardless of what Pike said, request processing permeated the atmosphere at Google, and deeply influenced the design of Go, whether Pike realized it or not at the time.
We can imagine Go with the same engineers but at Microsoft or Apple or Mozilla, and its evolution would have been different because its first clients would have been different. The times make the man, and the language!
Why? It doesn't seem inherently laughable to me, at least. Why couldn't someone write a browser in Go? I anticipate that it'd be easier to understand & easier to extend than one written in C++ or C.
I could be totally wrong, but have you had to optimize a tight loop in C/C++? It sucks and a lot of stuff is missing : simd, likely/unlikely branches, the compiler has a lot of trouble knowing when lines of code are independent and can be done in parallel (b/c const =/= immutable), in-lining can't be forced when you know it gives better performance (if you use GCC extensions you can alleviate a lot of this..but that's tying you to a compiler). Yeah C++ is generally faster than Go, but there is a lot of performance typically left on the table. The language kinda start to work against you when you start to dig down. So you slap some shit together and the compiler will do a decent job, but most C++ devs aren't even looking at the compiler output
But the elephant in the room is that more and more of our available flops are on the GPU and C++ isn't helping you there at all. Not only that, but the GPU is giving you way more operations per Watt (and that's what a lot of those people care about). And finally, when you throw stuff onto the GPU you are also leaving the CPU available to do other things. So there are a lot of "wins" there. As you illustrate, the areas of C++'s relevance is shrinking, and shrinking into the area that is very GPU friendly.
So the way I see it, C++ folks will start to write more OpenCL kernels for the performance critical pieces and the rest won't matter (Go or Clojure or whatever). The GLSL is kinda lame and too C99ish... so maybe someone will write a better lang that compiles to SPIR-V, and it's not exactly write-once-run-everywhere, but it could be much better than writing optimized C++ and it can run everywhere. It's more of the cross-platform-assembly C/C++ wants to be
Intrinsics are directly callable from C++.
> likely/unlikely branches
Most compilers have extensions that will allow you to do this (__builtin_expect and so on).
> in-lining can't be forced when you know it gives better performance
Again, most compilers have this, not just GCC, e.g. __forceinline.
> the compiler has a lot of trouble knowing when lines of code are independent and can be done in parallel (b/c const =/= immutable)
This is true, as aliasing is a real issue. The hardware itself has some say over this anyway, dependent on its instruction scheduling and OOE capabilities.
What you don't mention, however, is the fact that almost no other languages offer any of these, let alone all of them. Rust may be the exception here, although some of this is still in the words (SIMD, I'm not sure about the status of likely/unlikely intrinsics).
For GPU programming, if you're using CUDA, you're almost certainly using C or C++, or calling something that wraps C/C++ code. Not everything is suited to GPU processing anyway, there's still a lot of code that's not moving off the CPU any time soon that needs to be performant.
I'm not saying you can't get C++ to output the assembly you want - it just sucks trying to coerce it to do things that are honestly not that complicated. And even when you do get what you want you find you can't use the code anywhere else. To me that feels like a language failure...
> is the fact that almost no other languages offer any of these
I guess you missed my point. It seems to me that we're at a point where you no longer need these features as part of your core application language. The idea is that with OpenCL/SPIR-V we'll be able to
1- be more explicit and not fight the language (so even if you're 100% on the CPU it makes sense)
2- target every platform (you can finally write code for your GPU)
3- can be called from any parent language
You're right that not all performance critical problems boil down to tight shared-memory loops that can be thrown onto an OpenCL kernel - but my experience so far tells me that that's the vast majority of performance problems. So C++'s usefulness will shrink. But maybe my experience is biased and I'm off base. I haven't done much OpenCL myself - but I'm definitely planning to use it more in the future
You just have a header with different #defines for the different platforms you are going to ship on, or use a premade open source one.
If you want to ship on everything, you won't get full optimization stuff everywhere. It would be better if some of these features were in the standard, but in practice it isn't such a big issue for those two in particular.
1. Whatever C++'s weaknesses in this area are, it's superior to Go, so C++ programmers aren't going to switch to Go because of this.
2. Not everything is about raw throughput. You can't do anything latency sensitive on a GPU. Consider a game: the pixels get drawn on the GPU, and the physics might happen on the GPU, but you still have a ton of highly latency sensitive things that are going to have to be done by the CPU, such as input handling and networking. Also, even with low driver overhead APIs like Vulkan, you still have to have something on the CPU telling the GPU what to do. Finally, GPUs aren't good at branch-heavy workloads in general.
Obviously, Go has not replaced C++ usage. And, these days, I would see Rust as the more likely step from C++.
(Although, I still feel that it remains to be seen whether Rust will make a huge dent there; is memory-safety the killer-app feature that makes people want to use Rust? Do enough people feel that they Need To Use Rust to make it stick? I'm interested to see how that plays out.)
What Go has done, I think, is replaced interpreted language use (PHP, Python, Ruby) in backend code. Which makes sense, to me--those are already GC languages, so you're pretty familiar with the lay of that land. Generics may not make a huge deal for you because there were no generics to use in those other languages. And Go is quite a bit faster than any of the aforementioned interpreted languages.
While this is technically true, in practice it is a good deal easier to create generic data types in those languages because you don't have to switch back and forth between type-world and no-type-world.
If you used arrays in PHP, or Ruby, or Python, you can get those--with static typing!--in Go, either with slices for sequential arrays, or maps for associative arrays. I think that satisfies the vast majority of collection use-cases that arise in practical applications of those three languages.
(Note: I think generics would be a Good Thing for Go, and I think they'll probably happen at some point. They keep doing user surveys, and the user surveys keep bringing up the lack of generics as one of if not the number-one issue that users would like to see addressed.)
This argument, made often by the Go team, contradicts other arguments made by the Go team. Generics done like this have no type safety, which is the central reason for Go.
> If you used arrays in PHP, or Ruby, or Python, you can get those--with static typing!--in Go, either with slices for sequential arrays, or maps for associative arrays. I think that satisfies the vast majority of collection use-cases that arise in practical applications of those three languages.
Of course what everybody wants is trees, sorted maps, sets, ... WITH static typing.
> (Note: I think generics would be a Good Thing for Go, and I think they'll probably happen at some point. They keep doing user surveys, and the user surveys keep bringing up the lack of generics as one of if not the number-one issue that users would like to see addressed.)
No they won't. The real issue is that implementing them is pretty difficult in the compiler. Go's compiler is extremely, extremely simplistic, even to the point that it's badly written. It needs a LOT of cleaning up before anyone can reasonably contemplate adding generics.
Type safety is not the central reason for Go.
https://web.stanford.edu/class/ee380/Abstracts/100428-pike-s...
"The target
Go aims to combine the safety and performance of a statically typed compiled language with the expressiveness and convenience of a dynamically typed interpreted language."
This is what I mean by switching back and forth between type-world and no-type-world. If you implement a data type this way, I need to convert your no-type-world (interface{}) data to type-world data at some point after it pops out of your library.
> If you used arrays in PHP, or Ruby, or Python, you can get those--with static typing!--in Go, either with slices for sequential arrays, or maps for associative arrays. I think that satisfies the vast majority of collection use-cases that arise in practical applications of those three languages.
You do often (though not always) see "primitive obsession" in those languages, and Go encourages it even more so due to its only generic containers being the primitives provided by the language.
I don't mean to come off as a Go hater at all. I think it takes the pragmatic side of a ton of trade offs. But I do think that results in some weaknesses that people should be aware of.
One of the less talked about reasons Go is successful at replacing these languages is the devops story - essentially no runtime dependencies.
In most C/C++ world, you're better dynamically linking anyway due to most prior to Visual Studio 2012 it would determine the Service Pack/Dependencies of your host operating system and include that into the exe manifest.
Also Java never really worked in Devops. You need to a lot of ceremony to just open files and do simple regex work. Unfortunately Python went down the same path.
I think it's more prosaic reasons, like that's the language their job uses. People who use dynamic languages tend not to think of themselves as specialists in their stack.
Not to take a side in static-vs-dynamic linking or the language argument, but that is absolutely incorrect. Single-static-binary is a very significant reason for lots of people moving to Go: that, plus a good cross-compilation story make a lot of problems go away.
Vendoring dependencies is hard in general. It's much harder in dynamic languages.
Some evidence/examples:
- Look at how many different ways of packaging things that Python has.
- Ruby, which most people consider to be one of the scripting languages that got vendoring right out of the gate, still struggles with system libraries used to bootstrap the Bundler process on deployment targets.
- Node.js, another one which is considered to have gotten vendoring right out of the gate, has massive problems with its implementation: package assets in node_modules take forever to fetch/inflate deployment times and artifact sizes, and put strain on systems. People argue that the difference between "my node_modules directories have so many files I ran out of inodes" and "my golang binary is really big" is just a difference of degree, but it's a big difference regardless.
- Vendoring/deploying compiled/native dependencies are a massive hassle in dynamic languages as well: better make sure that you compiled those deps in a way compatible with your target system (a big hassle if you are, say, building an old Perl C/XS extension on OSX and targeting Linux for deployment), and make sure they all link correctly once there, and, if they link, hopefully they link with system libraries that don't have behavior differences from wherever you tested the code. And a lot of popular libraries have a native component.
- There's also the problem of dependency resolution. Several dynamic languages have hard-coded system library paths, which means that if your vendoring misses a spot, you might be loading an unexpected version of something, or failing to start. The "just put everything in the system lib path" ignores the reality of multitenant/multi-use systems, and as a whole 'nother piece of expertise.
- The popularity of Docker/containers is largely driven by the fact that they let you "statically link" your whole stack. That demand indicates that some folks, at least, found the vendoring story for dynamic languages difficult.
> People who use dynamic languages tend not to think of themselves as specialists in their stack.
This sounds suspiciously like "if you use $language you're an idiot/inferior". Spare me your arrogance and language elitism, please. There are specialists, generalists, experts, and idiots on every platform ever invented--in very, very similar proportions.
Go look at most recent go projects. Kubernetes will never have been done in PHP/Python/Ruby. It would have been a Java or C++ project. Same with cockroachDB, Docker, Etcd, Fleet, Lime, InfluxDB, Prometheus, etc.
There are algebraic types with pattern matching, sane generics, actually good type inference, strong types that will help you declare behavior only once (as in normal vs. modular arithmetic), and a huge push for not having undefined behavior (that is mostly but not completely successful).
Any of those (and yes memory safety) would be enough of a killer feature on my view. There are still some things I dislike in Rust, but compared with C it's a complete no-brainer.
For the first time, I think I'm motivated to learn Go. My big beef with C++ is there's no simple mode. Every single decision ever made in the syntax and libraries is geared for only the most complicated, high performance needed situations.
My problem is that 90% of my C++ code doesn't need performance at all, and could be Python for all I care.
To be fair, some of the modern changes are bringing some simplicity back. But, it still feels like wading through mud to write what should be easy things like file i/o, compared to Python or JavaScript. Using hashes and mixing hashes with other containers is so verbose and nit-picky in C++ compared to today's scripting languages that I just long to mix languages most of the time I'm writing general C++.
Beginner books meanwhile will be five, ten, or fifteen years out of date, and reliably give you a correspondingly wrong view of current best practices.
At this point C++ is almost an oral tradition. You need someone who knows what they’re doing to tell you which books to read and which parts of the language to ignore.
It’s horrendously difficult to learn if you do the usual thing and try to self-teach - and needlessly so, because the core of the language isn’t that complex.
And I write that as someone whose second language was C, and who loved C with a passion, for many many years. I didn't realise how much I was fighting the language, and I didn't realise how much in any C program the trees of incredibly verbose C-isms obscured the forest of what I was actually trying to do.
This.
The very first C++ code base I worked on was a monolith pile which had some thing like 50 features. There fore when you assemble something like that into a monolith your abstraction hunger kicks in and you run into all these crazy inheritance or template based work.
These days people don't seem to write stuff as such huge monoliths. And Microservices architecture has become quite a thing now.
Absence of many features in Go feel more like a positive than a negative. You get to eliminate a lot of complexity by default.
For example, calculus is more complicated that arithmetic and algebra. However, calculus can unify and show relations between different algebraic representations. For example, when I learned physics in middle school, I thought that remembering
delta position = velocity * time
but struggled with
delta position = initial velocity * time + 1/2 (acceleration * time^2)
When, I learned about derivatives and integrals, the formulas all made sense.
In my opinion Go suffers from the ability to abstract which is most represented in its lack of generics. Instead, of being able to represent operations in a generic way, you are forced to cut and paste, and cannot express the relationship accurately, much as how I considered the relationship between acceleration and position to be opaque before I understood calculus.
> Early in the rollout of Go I was told by someone that he could not imagine working in a language without generic types. As I have reported elsewhere, I found that an odd remark. [...] What it says is that he finds writing containers like lists of ints and maps of strings an unbearable burden. I find that an odd claim. I spend very little of my programming time struggling with those issues, even in languages without generic types.
Does he suggest we write containers from scratch every time we need them? There is a lot of nontrivial logic, which I'd prefer not to have to get right again every time. (Not to mention that would feel very much the opposite than "programming in the large")
> But more important, what it says is that types are the way to lift that burden. Types. Not polymorphic functions or language primitives or helpers of other kinds, but types. [... long rant why types are for mediocre people and interfaces are awesome... ]
I very much agree that interfaces are awesome and composition is way more useful than inheritance, but what does that have to do with generics?
If you have a function that takes a list of things and returns another list of the same things in sorted order, wouldn't you still want to have a way to keep track that the returned list contains the same things as the list you passed to it? That seems independent of whether the things are specified through types or interfaces.
Languages primitives for containers are a good idea, but the way Go eventually implemented that (hardwiring the few most-used containers into the language and making it really cumbersome to use custom containers) seems very unsatisfactory.
Yeah, because it cannot possibly be that many C++ programmers saw Go as step down.
Pike thought that C++ programmers would flock to Go in large numbers. That didn't happen, which means that Pike didn't really understand what motivated these C++ programmers. I don't think this summary gets that much closer to the truth.
You just paraphrased his point.
"C++ programmers spent a ton of time learning C++ so they're not going to switch to a language that isn't C++"
Which is basically saying that none of the design decisions that went in to Go matter to C++ programmers because at the end of the day, Go isn't C++. Which in turn is pretty damn uncharitable towards C++ programmers. A lot of technical criticism was offered, but it's easier to ignore that and focus on something you cannot change.
Many C++ programmers like the fact that it takes their informal rules of safe memory/thread management (use RAII, don't use raw pointers, don't use mutable shared state, be careful of iterator invalidation in containers) and formalizes them in the type system so the compiler can check them.
In addition, Rust has some nice features like pattern matching, algebraic data types, and Traits that are not yet available in C++.
Go on the other hand, was not as compelling to C++ programmers.
I understand the backlash against "magic," but this more a concern of "spooky action at a distance" rather than overhead.
Could somebody who understands the sentiment help articulate what type of solutions can be more easily expressed in Go than in say Scala (short of performance-centric cases? Or is this solely about performance-centric cases?).
This is something C++ makes exceedingly difficult (Linus Torvald's famous rant is half on this topic). This is also something Scala doesn't do well at, opting to provide a lot of features that make bits of code "prettier" at the cost of being able to see what is being built.