If I’d write an article called “Protobuffers Are Wrong” its content would mostly be mutually exclusive to this article. Because the #1 problem with Protobuf is performance: it has the need to do a ton of dynamic allocations baked into its API. That’s why I designed FlatBuffers to fix that. Most of the rest of Protobuf is actually rather nice, so I retained most of it, though made some improvements along the way, like better unions (which the article actually mentions).
Your comment and also some remarks in the article suggests to me that Protobuf was designed for Java and never lost that bias.
At the time Protobuf was designed, Google was mostly C++. It is not that unnatural to arrive at a design like Protobuf: 1) Start with the assumption that reading serialized data must involve an unpacking step into a secondary representation. 2) Make your serialized data tree-shaped in the general case, 3) allow arbitrary mutation in any order of the representation. From these 3 it follows that you get, even in C++: 4) the in-memory representation must be a dynamically allocated tree of objects. FlatBuffers questions 1) and 3) :)
To me it sounds like your issue is with whichever protobuf implementation you were playing with when you checked it out.
There are protobuf libs that will do the job fine without all the allocations. Are you aware there are other implementations, and that protobuf is by now a bit of a protocol in and of itself… ?
Link to these magical allocation-less Protobuf implementations?
At least internally to Google, Protobuf allocs are a huge cost, which they’ve so far been unable to eliminate. The best they can do is arenas. If it was easy to fix, they would have done it by now.
I can imagine ways in which you could read a Protobuf without allocations, but it be a) completely incompatible to the current API, b) not have O(1) or random access to data (unlike FlatBuffers) and c) not allow mutation. That would thus be entirely useless to most users of Protobuf.
I’m aware of nanopb.. using that without malloc is only possible in very limited situations, where you might as well have used an even simpler serialization method. It has some serious limitations and is.. slow. Compare that with FlatBuffers, which can be used with even less memory, is very fast, and can also be used with more complex datasets.
I use nanopb quite effectively, so none of your issues bother me in the slightest. Nevertheless it demonstrates that its quite possible to use Protobufs without any of the original issues you claim make it unsuitable.
Most of the “reasonable” ones in this list (the ones not using a large language runtimes) will have their memory usage dominated by graphical assets (fonts, and maybe textures/buffers derived from them, etc). The actual efficiency of the widget data structures is going to matter little in comparison.
I worked on an immediate mode gui a while ago (https://github.com/google/flatui) and was proud of the fact that most memory use was transient, i.e. it could layout and render hundreds of widgets while using only a few hundred bytes of memory! (code: https://github.com/google/flatui/blob/master/include/flatui/internal/flatui_layout.h). That is, until you actually try to render something, and fonts/textures dwarf memory usage of the core of the system. Oh, and having to link in lots of i18n libs etc that dwarf the code size of the core gui system.
So a truely efficient library would need to render all fonts with vectors, on the fly. I presume some of the lower memory use ones may use system font rendering, so it doesn’t count towards their own memory usage, like all the GL based ones.
Surprised by dear IMGUI, which should be much lower since a) immediate mode can easily be more efficient memory wise than retained mode, and b) it uses a tiny built-in font.
The numbers next to Dear ImGui are mostly flat cost of SDL + OpenGL/Vulkan setup which need to create multiple framebuffers. Measure of that cost is likely to grow with resolution.
There’s a line saying: “SDL (14.0 mb)” above it which gives an indication of that fixed cost. Whereas e.g. “Nuklear (rawfb)” is poking into a framebuffer it doesn’t own, and curiously there’s no equivalent “Nuklear (SDL+GL)” for comparaison of those fixed costs. Rawfb is also probably a bad idea for perf reason (we could also render Dear ImGui with software rendering but in most situations it is silly), and users of accelerated graphics API in their app are already paying the flat framebuffer costs. Which is to say that even though that page is correct, caring about those numbers means ignoring the big picture and can be largely misleading. If there was a measurement to do outside of productivity and flexibility, it would be more interesting to measure prolonged cost of equally elaborate apps - but that’s impossible to achieve because no one is going to write equally elaborate apps for all those toolkits.
Yup makes sense. Would be great if these tests can be run somehow not counting framebuffer costs. Or maybe they can be run forcing each window to be some very small size.
See, everyone has a different thing they find important about Rust. He suggest abandoning zero cost abstractions (letting stack/inline allocation be decided by the compiler instead) which I think would remove most of the power of the language. Default thread safe primitives sounds really wasteful too.
I think languages similar to Rust but with less complexity will be a very cool space to watch going forward. All sorts of interesting language designs possible.
I’m working on a language that does “Rust-like” things, but unlike the article, what I am going for is the efficient memory management, in this case the inline structs (zero cost abstraction) and compile time memory management (automatic lifetime analysis). The biggest difference with Rust is that it is fully automatic: when ownership can’t be determined at compile time, by defaults it falls back to a reference count increase at runtime (in Rust this would just be an error). The advantage is zero annotations, and a language that can be used mostly by people that don’t even understand ownership. There may be ways to explicitly (optionally) enforce ownership in the future, for those who want more control.
Details on memory management: http://aardappel.github.io/lobster/memory_management.html
I honestly tried several times to get into it but I don’t know, it just didn’t click yet.
I find golang much nicer to work with if I need to do what rust promises to be best at.
I won’t give up on it yet but that’s just my feeling today.
I’ll likely take some heat for this but my mental model has been:
Go has found its niche in small-to-medium web services and CLI tools where “server-side scripting languages” were the previous favorite. Rust has found its niche in large (or performance-sensitive) interactive applications where using GC is untenable. These aren’t strict boundaries, of course, but that’s my impression of where things have ended up.
I agree with the mental model, although I usually think of Go as the Java for (current year).
The tooling is light years behind though
“go fmt” offers a standard way to format code, which removes noise from diffs and makes code other people have written more readable.
“go build” compiles code faster than javac.
The editor support is excellent.
In which way is the Java tooling better than Go, especially for development or deployment?
How is the debugger these days?
When I was doing go a few years ago, the answer was “it doesn’t work”, whereas java had time-travel debugging.
Delve is a pretty great debugger. VSCode, Atom etc all have good debugging support for Go, through the use of Delve. Delve does not have time-travel, but it works.
Packaging Java applications for Arch Linux is often a nightmare (with ant downloading dependencies in the build process), while with Go, packaging does not feel like an afterthought (but it does require setting the right environment variables, especially when using the module system that was introduced in Go 1.11).
Go has some flaws, for example it’s a horrible language to write something like the equivalent of a 3D Vector class in Java or C++, due to the lack of operator overloading and multiple dispatch.
If there are two things I would point out as one of the big advantages of Go, compared to other languages, it’s the tooling (go fmt, godoc, go vet, go build -race (built-in race detector), go test etc.) and the fast compilation times.
go build -race
In my opinion, the tooling of Go is not “light years behind” Java, but ahead (with the exception of time-travel when debugging).
My three favourite features when developing Java:
Sure, it would be nice if there was a single Java style, but pick one and use that, and IDE’s generally reformat well. Also, the compile times can be somewhat long, but for plain Java thay are usually ok.
Note that I have never had to work in a Spring/Hibernate/… project with lots of XML-configurations, dependency injections, and annotation processing. The experience then might be very different.
Just the other day I connected the debugger in my IDE to a process running in a datacenter across the ocean and I could step through everything, interactively explore variables etc. etc. There is nothing like it for golang.
That sounds very similar to this:
“I’ll likely take some heat for this but my mental model has been:”
All kinds of people say that. Especially on HN. So, not likely. :)
“These aren’t strict boundaries, of course, but that’s my impression of where things have ended up.”
Yup. I would like to see more exploration of the middle in Rust. As in, the people who couldn’t get past the borrow checker just try to use reference counting or something. They get other benefits of Rust with performance characteristics of a low-latency GC. They still borrow-checker benefits in other people’s code which borrow checks. They can even study it to learn how it’s organized. Things click gradually over time while they still reap some benefits of the language.
This might not only be for new folks. Others know know Rust might occasionally do this for non-performance-sensitive code that’s not borrow-checking for some reason. They just skip it because the performance-critical part is an imported library that does borrow-check. They decide to fight with the borrow-checker later if it’s not worth their time within their constraints. Most people say they get used to avoiding problems, though, so I don’t know if this scenario can play out in regular use of the language.
I agree, for 99% of people, the level of strictness in Rust is the wrong default. We need an ownership system where you can get by with a lot less errors for non-performance sensitive code.
The approach I am pursuing in my current language is to essentially default to “compile time reference counting”, i.e. it does implement a borrow checker, but where Rust would error out, it inserts a refcount increase. This is able to check pretty much all code which previously used runtime reference counting (but with 10x or so less runtime overhead), so it doesn’t need any lifetime annotations to work.
Then, you can optionally annotate types or variables as “unique”, which will then selectively get you something more like Rust, with errors you have to work around. Doing this ensures that a) you don’t need space for a refcount in those objects, and b) you will not get unwanted refcount increase ops in your hot loop.
Ha ha, just by reading this comment I was thinking ‘this guy sounds a bit like Wouter van Oortmerssen’, funny that it turns out to be true :-) Welcome to lobste.rs!
Interesting comment, I assume you are exploring this idea in the Lobster programming language? I would love to hear more about it.
Wow, I’m that predictable eh? :P
Yup this is in Lobster (how appropriate on this site :)
I am actually implementing this as we speak. Finished the analysis phase, now working on the runtime part. The core algorithm is pretty simple, the hard part is getting all the details right (each language feature, and each builtin function, has to correctly declare to its children and its parent wether it is borrowing or owning the values involved, and then keep those promises at runtime). But I’m getting there, should have something to show for in not too long. I should definite do a write-up on the algorithm when I finish.
If it all works, the value should be that you can get most of the benefit of Rust while programmers mostly don’t need to understand the details.
Meanwhile, happy to answer any more specific questions :)
I don’t understand, and have never understood, the comparisons between Go and Python. Ditto for former Pythonistas who are now Gophers. There is no equivalent of itertools in Go, and there can’t be due to the lack of generics. Are all these ex-Python programmers writing for loops for everything? If so, why???
Not Go but Julia is more likely the Python of 2019.
Rather than the “rule of 2” (classic DRY) and the “rule of 3” (what this guy is proposing), have “the rule of N”, where N depends on ratio between the size of the duplicated code and the cognitive complexity of the abstraction.