I know Rust also offers arenas and other purpose built tools for more optimized allocation strategies, but Rust doesn't seem like the language you would reach for if your number one priority is memory performance.
It seems like there is a necessary trade-off between truly top-end memory performance and memory safety.
Yes, you can trash your cache in all these languages if you choose to do everything with references to a multitude of individual heap allocations... but in Java-likes you don't have the choice not to do that
What instead is needed is a intermediate language, that takes the constructs of object orientation and the instruction flow and allows to rearrange them for maximum memory efficieny. Like, strip those OO-bjects into arrays, or directly pack them into hot-loop structs that have little regard for the Objects they started out with.
Can someone from the old guard tell me, how often we have been here in the language design cycles through the desert?
That's why you finally stop teaching Java in high schools.
Functional programming is much simpler, but we don't spend years hammering the concept into people's brains.
I'll just use a Kleisli
XD
And IME moving from FP to OOP is MUCH easier than the other way around.
Don't really have the knowledge to compare iterative vs functional for a complete beginner, though I suspect that even there moving from FP to iterative is easier.
I think Jonathan Blow's JAI is trying to do something like this. Unfortunately it isn't exactly available yet, or it wasn't last time I looked.
I especially like Java’s plan of introducing them: according to the latest design iteration, there will be 3 buckets of objects: current identity-having ones, value classes and primitive classes.
The second category would drop identity, but will keep nullability (for example two DateTime instance of the same value will be considered equal on a VM-level, allowing for optimizations like allocating DateTime’s inside an array serially, or stack-allocation. Nullability is important because there is no sane default value choice for for example a DateTime. But this value can be encoded cleverly similarly to what Rust does with optional)
The third category will loose nullability as well and current primitives will be migrated to it. So a ComplexInt “class” will be possible to implement with “zero” overhead.
My point being that there are two ways of improving performance, either showing more knobs to manipulate, or to raise the abstraction level, allowing cleverer optimizations. C# does the former, while Java never went that route, and I think that the latter approach better fits a managed language and can easily push it for like 90% better performance at a fraction of developer complexity.
Zig has closely aligned goals.
Small string optimizations, while nice (and probably do average out to being beneficial), aren't always needed, and the extra generated code for handling both cases could make it not worth it if you've got a fast allocator, and can even make some operations just outright slower. (and if your code doesn't actually have strings anywhere near hot loops, all you get is a larger binary). File paths, for example, are often large enough to not fit the small case, but still small enough where even the check for whether it is small can be a couple percent of allocation/freeing.
Being error-prone, though, is something that I can agree with. That's the cost of doing things manually.
(I'd also like to note that malloc/free are a much more important case of a bad abstraction - they have quite a bit of overhead for being able to handle various lengths & multithreading, while a big portion of allocations is on the same (often, only) thread with a constant size, and said size being known at free-time, which is a lot more trivial to handle; not even talking about the cost of calling a non-inlined function spilling things to the stack)
Also, I’m not sure the added conditional branch will increase the binary too much, and the reason it is inside the c++ stdlib is that it was likely measured and proved beneficial.
But I do agree that maybe your allocation example is a better one, though the solution to that is perhaps a full GC, which does have a few tradeoffs (which are worthy to take more often than not).
Recommending a GC is hard for me though; over your system malloc/free, maybe, but alternative allocators can be very fast, without the drawbacks of pauses (or slower execution as a result from a non-pausing GC).
I've done video4linux stuff in Go, and passing an unsafe.Pointer to a Go struct in an ioctl() worked fine, which tells me that Go structs are isomorphic to C structs. Even though Go has garbage collection, it allocates everything it can on the stack, so only long-lived shared-between-goroutines objects are subject to garbage collection.
Go abstracts concurrency, completely removing all concurrent features from a language except for the "go" keyword (that launches a goroutine - which is basically a tiny virtual thread), channels (which are selectable queues) and "select" keyword that waits for the first "input" from a static set of channels.
There's a few features that could improve it, like proper ADT's, and it's a bit lacking in expressiveness for me to choose it for personal hobby projects, but I would recommend it any time for general-case professional software development.
It ain’t running circles around any managed language, with perhaps the exception of Python.
C#, Java, JS all have comparably good performances, sometimes much better - e.g. when GC can’t be avoided.
If you have any data (or better yet, benchmarks I can run on my own machine), I'd very much like to see some hard numbers.
Benchmarks are hard to do right but this one does actually measure GC quite well: https://benchmarksgame-team.pages.debian.net/benchmarksgame/... (For managed languages. It is not really a fair comparison between non-GC and GC-languages)
So all in all, for bigger programs it is hard to do a good comparison, but there is exactly where JIT compilers shine and the memory tradeoff and the like brings their return.
It seems like it's a pretty good option to have your infrastructure code implemented in a systems programming language like C or Rust (probably AWS or GCP is doing this for you), and just implement your business logic in Go as the type of small, well-defined programs you're talking about.
And we will have to disagree on C#/Java/Python being unreadable mess. In my experience all 3 can be written in a really well maintainable way. I don’t have much experience with Go, but out of these, I would vote for it as the least maintainable (as just because each line is trivial to understand, doesn’t make the whole program flow easy to read. Otherwise why not just write assembly, every line is even more trivial there)
That's true, but that is generally true of any (non-toy) language. But in the modern world of rapid development, it matters how hard is it to write code in a non-maintainable way - i.e. how well it tolerates modifications by different people. And to me, it seems easier to write readable code in Go than it is to write unreadable code.
It seems to come from the lack of features - Java, Python and C# have too many features, and any problem can be solved in N different ways, each one with its own warts. If you want to work on a wide range of codebases, you have to know each one of the approaches and their warts and footguns.
Meanwhile, Go feels like it really reached the "there should be one obvious way to do it" ideal of Python, while Python has over the years evolved into something more Perl-like. Want to build a concurrent application? Chose your tradeoff - either you get CPU scalability (multiprocessing) but lose memory sharing, or you get a simple concurrent model (threading) that isn't scalable, or you get I/O scalability (asyncio) at the cost of function coloring, error-proneness and a single-threadedness. Go solved the whole thing with the goroutine model - internally it multiplexes coroutines onto a set of OS threads, but all blocking calls are wrapped by Go runtime which makes every coroutine behave and feel like an ordinary thread, without the massive memory use of OS threads.
I would go as far to claim that Java is an easier language than Go, or at least in the same ballpark.
I think you're asking for technical details, but I'm afraid I don't know Java well enough to do an objective comparison. I'll try with a subjective explanation or why I think so.
I've learned Go in 20 minutes following the Go Tour. Few months later, I feel like there isn't a single thing I don't know about Go. It's dead simple. When I open a Go repository, it's easy for me to get into the codebase, as all code is more-or-less the same.
I've learned Java back in high school, and to this day I don't feel like I "know" the language. I've tried reading some Java repositories, and every time I feel like there's some kind of friction - some implicit knowledge about it that I just don't understand.
Maybe it's just me, and I haven't spent enough time learning Java. But then again, I've spent even less time learning Go, and yet I have a much easier time using it. That's what I mean by "a very small language".
Languages with very little type information, e.g. dynamic ones, tend to be quite hard to maintain, unless the original developers kept the discipline of good naming and verbose commenting. Go and Java with their somewhat static, but limited typing and elements of dynamism (interface{}, Object, reflection), sit somewhere in the middle between PHP/JS and Rust/Scala/Haskell.
Languages with little expressive / abstraction power, so the ones limited in features or low-level are also often hard to maintain, because you have to reverse-engineer the high-level stuff from all the details you see. Take assembly as an example - while it may be quite obvious what the program is doing at the bits and bytes level, understanding the sense of that bit-level manipulation may be a much harder task. The assembly language might actually be very simple, but that does not help. I remember when we had a MIPS class, the whole specs was just a few pages, could be learned in an hour.
Regarding concurrency, I wouldn’t choose existing reactive frameworks and what not for a new system. Java will soon get Project Loom, which will introduce Go-like virtual threads - so that one can write a web server that spawns a new thread for each request as well. Since the Java ecosystem is very purely written almost exclusively in Java itself (no FFI), basically everything will turn automagically non-blocking.
If you look at other examples on that site, Go and Java are roughly the same, but with some variance:
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
The source code is shown —
binary-trees Java #7 program
https://benchmarksgame-team.pages.debian.net/benchmarksgame/... binary-trees Go #2 program
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...Having read the rules it's difficult to know what's considered "fair" for this test - all GC tuning is off the table, sure. But what's bugging me is "Leaf nodes must be the same as interior nodes - the same memory allocation." So what constitutes "the same memory allocation" - literally the exact same call to some opaque internal allocator? If so, shouldn't Java also have to disable JIT to be fair?
Let me offer an alternate interpretation: I will do the same memory allocation if I need to allocate a node, but if my language lets me not allocate a node yet still use that node why should I? Or an alternate argument if you don't like that one: Why must my "node" be `Tree`, rather than `*Tree`?
A central idiom of Go is that zero-values of a type can be useful; a two-line change, no new special-cases, no pooling or such gauche hacks:
// Count the nodes in the given complete binary tree.
func (t *Tree) Count() int {
if t == nil {
return 1
}
return 1 + t.Right.Count() + t.Left.Count()
}
// Create a complete binary tree of `depth` and return it as a pointer.
func NewTree(depth int) *Tree {
if depth > 0 {
return &Tree{Left: NewTree(depth - 1), Right: NewTree(depth - 1)}
} else {
return nil
}
}
I'm sure someone will tell me I "optimized away the work" - but in the end I believe I'm making exactly the same number of method calls on the same type of receiver. If that's not the work, what is?Seems like moving the furniture around.
No doubt I've misunderstood.
afaict the Java etc programs could be re-written using null where you used nil — please explain why you think that isn't correct.
- Practically, Java would rather have to `return 3` when it detects a null child, effectively precomputing the penultimate level.
- Semantically, Java could no longer distinguish between an absent child and a child with no children.
Honestly, I have other variants that still don't use pooling but are less idiomatic; I find this exercise is begging the question hard. Any tools, however idiomatic, the language is giving you to reduce the effects of allocation seem to be off-limits for GCd languages. Whereas then e.g. C can just throw them all in a third-party pool library. And JIT languages are presumably allowed to fuse anything they want.
And we could add some more rules ("distinguish between an absent child and a child with no children") to reject those Java etc programs.
I could rewrite the Go program to return 3 too, but I doubt you’d accept that.
I can only conclude the benchmarks game is bad-faith bullshit at this point.
Kudos for actually bothering to "read the rules".
None for ignoring what you read.
The answer to "… but if my language lets me not allocate a node yet still use that node why should I?" is — Because allocate a node is the basis of comparison with the other programs!
Change that for the Go programs and you change that for all the other programs; otherwise just special pleading for Go lang.
If `ch` is a channel, then this expression means "value obtained from reading from the channel":
<-ch
And this expression means "write value x into the channel": ch <- x
Both expressions can be used as a case inside select statement: select {
case val := <-ch:
// ...
case ch <- val:
// ...
}
Which will execute exactly one case, depending on which channel becomes "ready" first - channel is ready for reading if there is another goroutine blocked on a write operation, and ready for writing if there is another goroutine blocked on a read operation.You say you're from C background - if you've ever worked will file descriptors you will notice that channels are basically userspace file descriptors. Channel reading and writing is isomorphic to read() and write() syscalls, and select keyword is isomorphic to select() syscall.
Hopefully this clears up the whole channel syntax thing. I just hope you weren't trolling.
This is a very bad mental model because channels operations cannot be canceled (without using select on two channels) or return any error status (at all).
The syscall comparison was made to give intuition about general behavior of channels to someone with a background in C. Of course it's not completely identical.
You might as well explain channels in terms of any blocking operation if the bar for "isomorphic" (now backtracked to "intuitively" I guess) is that low.
Unless, of course, system calls can be modeled as operators over sets. In which case, please tell me how.
Julia is fast, easy to read, and easy to write. But it's not easy to maintain. There is a direct tradeoff between dynamism on one hand making things easier to read/write and static enforceability on the other making it easier to maintain.
Then there were BLISS, Mesa and PL/I, but the OSes that made use of them lost to UNIX, so.
With exception of Mac OS, written in Object Pascal and later ported to a mix of Object Pascal and C++.
Having said this, plenty of alternatives with AOT compilers exist nowadays.
The only thing C has going for it, is historical weight, UNIX/POSIX ecosystem, and some domains that are closed to any alternative suggestions, due to tooling or cargo cult against alternatives.
Code being easier to read and maintain is a function of how close it is to human semantics. The more the algorithm is presented in terms and notations humans like and find familiar, the easier. Code being performant is a function of how close it is to machine semantics, the more the algorithm is presented as steps that the machine likes and finds familiar, the faster it will run, as the machine is doing less to execute each step.
There is a fundamental tension between the 2, even if compilation from high level languages might, at first glance, give us the illusion that we can have both. We can't, not in general. We can only do it for a class of human semantics that C++ folks call "Zero-Cost Abstractions", the set of abstractions that can be completely erased without a trace by the time you get to the executable.
But otherwise, there is a fundamental cost to making code more readable by humans: making it less readable by the machines that will execute it. This is a reflection of the fundamental alienness of computers, what they find quite easy you find quite hard and vice verca. Optimizing for huamans means generality and ruthless hiding of details, optimizing for machines is all about special cases and ruthless exploitation of assumptions.
(Incidentally, C is not all what it's cracked up to be. Generic containers, off the top of my head, resort to using void* pointers for data and function pointers for operation, which has a runtime cost besides being unsafe and error-prone. Templates in C++ can aggressively inline types and operations for you, on the other hand as if you haven't written generic code at all, no wonder templates is the poster boy for C++'s 0-Cost abstractions. Another example I hear often is how pointer semantics in C and C++ makes it extraordinarily difficult for the compiler to optimize array and memory operations, whereas a language like Fortran make it easier by not having pointers.)
I use JavaScript and C++ for different things, sometimes in the same day. (And python and PHP and others, but this is not relevant.)
Believe me, JavaScript can be a real head scratcher compared to C++.
And now for the purists: No, I don't use all features of C++, only the minimal necessary ones for the problem I have to solve. This ridiculous idea that you are not using C++ if you are not using every single language feature is what makes programs difficult to write and maintain.
The nice thing about D for me is that you can generally banish the unreadable metaprogramming code to a library.
Swap out { } for Begin End, and make a few other changes, and you've got Pascal. Single pass pascal compilers have been faster (at compiling) that almost anything out there since Turbo Pascal 3.0 for MS-DOS.
Modern versions, such as Free Pascal, Delphi and Lazarus also deal with strings in a manner that totally avoids needing to manually manage memory. The GUI builders are awesome as well.
I don't know if transpiling will get you there, because for instance if you're transpiling a dynamic language, you're going to have to output C that is essentially emulating all those dynamic language features, so it might be faster than say, the original Python, but it's not going to be as fast as a pure C implementation.
If anything, C has an even bigger advantage on modern CPUs because it has easier access to things like vectorize/SIMD intrinsics. It is also easier to tweak your data dependencies to help the branch predictor.
Have you seen the ridiculously complex optimizations that C compilers do to maybe turn some shitty for loop into vector instructions? C is terribly bad fit for this use case and hardware tries to get closer to C than the reverse.
C++, Rust but even C# and Java has much better SIMD support than C has.
EDIT: It really doesn’t help C (and some of the listed languages as well) that they are very imperative. SIMD is exactly the place where pureness and some form of FP is much better at allowing these kind of optimizations (a map of a pure function can be “trivially” optimized into vector instructions, while it is really hard to decide whether this for loop is safe to convert, and it is really up to the heuristics of the compiler. A bit of rearrangement can cause a failure to optimize, resulting in a huge drop in performance)
Any language can have such intrinsics as extensions, for example D, Rust, C++, Swift, .NET, Java (as preview).
If you transpile something to C it does't mean it will be fast. You can write slow C code (or transpile something to C that will be slow). The compilers are not the issue here.
Fortran is fine. Also lua (using the luajit interpreter you get really close to C speed) and julia (except for the atrocious startup time).