Is zig lang faster than rust?

At the end of the day programs have inputs, do some transformation on the input data and produce outputs. As far as I understand any programming language can be used to produce the same outputs from the same inputs. They are all equally capable that way. Something about Turing Completeness Turing completeness - Wikipedia...

That suggests that any program written in any programming language can be transformed into any other language. Rust to C, C to assembler, assembler to binary machine instructions. No reason then we can't have JS or Java to C and so on. That is the compiler I'm talking about.

None of this is in any way dependent on how a language works, not object orientation, functional, with garbage collector with out, with or without borrow checker. They are all equally capable.

That is all theoretical of course. It seems very likely such a transforming compiler would be very hard to write. So we don't. We just design languages with syntax and semantics like C, Rust, Pascal, that make it much simpler to transform source into fast executables.

But see also the Turing tarpit - Wikipedia.

2 Likes

Exactly. It says:

Beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.

Which I think nicely sums up what I have been saying.

Using the size of the hello world binary with default settings isn't a good reason to argue that Rust is not lightweight. I would like to apologize for this conclusion.

The reason why I got involved in this thread was that I felt the discussion here was a bit biased. But that was just a non-well-founded feeling.

I was searching for aspects where Zig could be better than Rust because I'm interested in Rust's strengths as well as weaknesses. And I was simply "surprised" by such a huge binary size in Rust, and I still find it "noteworthy". There may be many reasons why it's like that, and why I shouldn't compare it to C, etc. etc. But in the end, using the operating system I have, using the tools I have, etc., I am not able to get a small hello world binary.[1] I'm looking forward to try Rust in an embedded context, where different rules will apply (and where I have no console to print out anyway, as there is none connected).

I used the binary size as an example where I observed Rust feeling less lightweight to me. In my posts above, I also talked about:

  • dependencies in idiomatic code
  • compiler and language complexity

I have spent literally HOURS in Rust trying to solve very easy problems. Then at other times, I solved problems in a couple of seconds where I would have needed difficult and potentially dangerous constructs in C to achieve the same.

There were also compiler issues I came across when using nightly Rust and unstable features (such as this and that, or undocumented aspects of the type system[2]). Of course, I'm also comparing apples and oranges here, because I never used any unstable C features yet. But I need the unstable features in Rust to be able to use its type system in the way I need. In C I simply do (void *)(…).

I didn't want to say Rust is bad, but Rust still feels not lightweight to me, due to a lot of reasons. Maybe that would change in future.

Edit: To be fair: Rust also doesn't feel overly bloated to me, particularly not in regard to runtime overhead, where it really rocks, considering what it has to offer!

As I said above: My apologies for the implied "conclusion".

Even if this is a Rust forum (and thus naturally biased), I'd appreciate if I can "note" any downsides of whichever language (e.g. in corner cases such as hello world, or when using default settings). Nonetheless, I'd like to thank you (and other posters) for pointing out some of the reasons why the binary size in Rust is bigger (and how to make it smaller when needed). This helps me with understanding Rust's behavior and design choices better.

I tried to make a point about overhead, not about efficiency. Maybe that's the same? Not sure. For sure, I didn't define overhead properly. I just made a comment that I already regretted two messages above.

Edit 2: And let me say once more: overhead isn't necessarily a bad thing.

Yes, I believe my feeling about the huge dependency tree in Rust might be misleading. I still notice that I've never used as many dependencies for simple tasks before I started using Rust. That might be good or bad…


  1. I didn't try hard, though. ↩︎

  2. regarding the reference ↩︎

2 Likes

Oh! Absolutely! As long as you don't care about memory consumption and execution speed any language can be converted to any other language. That is what Turing Completeness means.

Sadly, in practice these things do matter. Slowly but surely even new generation of “why spend 100 bytes on something if you can spend 100500 bytes on the same thing” starts to noticing that (that naïve belief in turing completeness and equality of different languages is why we have slow-as-molasses websites everywhere: old projects which included bazillion warts and quirks in 1MB of convoluted JavaScript code are replaced with 100Kb of very clean ES2021 code… which is then transpiled into 100MB of transpiled code which takes 100 times as much memory and 100 times slower than what you had before… then “code red” is declared… and eventually marketers are left with the task of how to explain that replacing something that worked perfectly well with something 10 times slower, 10 times bigger and with 2 times less features can be classified as “progress”).

Oh, sure. But if you would actually look on how Turing Completeness is usually proved then you you'll see that it's not uncommon to convert O(N) algorithm into O(N²) algorithm and then into O(N³) algorithm when you go back.

This makes Turing Completeness an interesting theoretical property with limited practical applicability.

7 Likes

So we are in agreement then. Which circles us back to my statement that a language has to be designed for generating performant code, syntactically and semantically (Well, I guess I posed it as a question for discussion).

I suspect that all those web/cloud developers that choose to work in JS or Python or whatever have not done so because of any thoughts Turing Completeness. Likely they know less about it than I do. They are seduced by the apparent simplicity of such languages.

< anecdote>
I was just on a call with my partner in our little business. He told me how his cloud processes were crashing out, that they were using 100% of CPU and most of the machines memory with only 17 clients connected! He is a Python head and has been ignoring my use of Rust for the year we have been developing things. Which he looks at with disdain pointing out that he can develop things much quicker in Python. I guess I have to take a look at it...
< /anecdote>

Well actually, Sun GraalVM can do escape analysis to elide a lot of heap allocations to be simple stack allocations, and it can even use LLVM to emit native code instead of Java bytecode.

GraalVM can improve Java performance significantly and is the Java community's "A New Hope".

GraalVM is also a polyglot compiler with support for JavaScript, Ruby, Python, Kotlin, etc.

I imagine doing native Rust-Java interop might also be on the horizon.

The Spring ecosystem (which is heavily reflection/inversion of control) is also backing GraalVM as an early adopter for sweet speed boosts.

Sources:

https://www.graalvm.org/examples/java-performance-examples/

https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/

2 Likes

Furthermore, a future Java version may get User Declared Primitive Types, which will be stored directly on the stack, which will give Java programs potential speed improvements.

https://openjdk.java.net/jeps/401

In conclusion: many old programming languages are still evolving, and may change the competitive performance landscape.

The key innovations of Rust are probably not attainable in those languages, though.

Is rust currently do that or not

In general Rust does not need to perform the kind of escape analysis that Java does because Rust doesn't automatically heap allocate everything in the first place.

3 Likes

But LLVM is capable of removing boxing in some cases. A trivial example:

I've seen services written in Python which can support a lot more than just 17 clients. The issue here is not language (although Python is not all that fast) but complete disdain to the idea that resources are valuable and should't be wasted.

If you would convince him to switch to Rust — he would manage to achieve the exact same thing in Rust. It's not that hard. You may write crazy wasteful program in any language, no matter how efficient.

It will die the exact same way all previous “hopes” died. Java is slower, than Rust or C++, yes, but difference is minor.

Main reason Java programs are slow is the same as Python: frameworks written by people who don't care to save resources.

Obviously. Since the key advantage of Rust is the fact that it makes programming-without-care so painful that people who think computer resources are limitless use something else.

4 Likes

In a recent talk Andrew Kelley about why zig lang is more speed than Rust.
In his own words

I was going to say I’m going to be Rust now. So for that one, I’ll circle back to the other example you brought up, which was self-hosting the Zig compiler and the speed-ups that it brought. So to be clear, the speed-ups that it brought are almost entirely from the fact that now I know what I’m doing, right? And it’s the second version of something. You’re always going to do better on the second version of something.

That’s where those speeds come from.
But I can tell you what I did, right? Now, what is the better thing than I did, and therein kind of lies the justification for my claim?

So what I did is I learned about data-oriented programming. I developed a better intuitive model about how the cache system of CPUs work. And the fundamental premise, or observation I should say, is that if you touch less memory, then the CPU cache will get to have more hits for the memory that you actually do touch. That’s the whole observation. So based on that observation, you can make a bunch of code changes and then your code gets faster.

So one of the ways that I did this in a self-hosted compiler was I found the place where we created a lot of objects on the heap and the objects that we were creating on the heap were IR instructions. So this is a part of a compiler that is an intermediate representation that you use to pass from one component of the compiler to another component of the compiler. You admit these intermediate instructions for the second component to consume. And it’s code, right? So you’re generating many of them in memory based on the code that the user types.

So based on the observation about CPUs and this knowledge, my hypothesis is, okay, if we make these objects take up less memory, then not only will that just use less memory in the compiler, which is good, it will reduce the pressure on the cache of the CPU and therefore make the code faster. And this turned out to be completely true and ended up being something like a 35% speed-up. But not only that but that pattern-

Yeah, it was huge. And that pattern also existed in three other places. So I got that big of a speed-up three other times for doing the same strategy.

It’s always amazing to me the impact of increasing that cache hit rate or cache locality,

It’s all about algorithms and whatever, but man, that is a substantial speed-up.

Yeah. But let me make my point with Rust though because I do have a way to tie it in.

So in order to do this optimization, this data-oriented design reworking, one of the core components is an untagged union. And so in Rust, these are enums, but they’re tagged. There was a tag for this data structure, but you put it in a separate array and that’s part of the strategy. So you have one array up here and it’s each one is a byte, and the byte says this is the kind of instruction–it’s like add, subtract, store, whatever. And then in a different array, each element contains the instructions data and they’re a fixed size. So if you want to find out the tag, you’ll look up index number one and the tag is in this array. If you want to find out the data, you go look in the other array, index one, there’s the data. These are called instructive arrays.

And the point of this is that if you tried to put all this data in one array, you would be wasting seven bytes of padding because if you only need one byte for the tag but then you have like a pointer size field here, you have to have padding there. So you just put them in different arrays and then like poof, the padding goes away and that’s better for the cache, right? You’re not putting padding in the CPU cache.

Total waste. So, the problem is that you can’t model this in Rust without unsafe. You have to use unsafe, which is fine, you could do unsafe, right? But no one wants to do that in Rust. No one wants to use unsafe because people come to yell at you and make you feel sad. So they don’t.

Yeah. So people don’t do it, right? In practice, people try to write safe Rust code, but you can’t fully exploit the hardware of your computer is fully safe Rust code. So, in practice, these strategies are available to Zig programmers to write faster code. And here’s the kicker, this code is safe in Zig because we have safety on untagged unions. Like we have both. It’s safe

So this is an example. I mean, it’s one little use case. I mean, it’s not contrived, it’s a real use case. It’s the self-host compiler, but it’s a use case where it’s safe in Zig, but if you tried to write this code in Rust, it would be unsafe or it would perform worse.

2 Likes

Reminder that, as alice explained earlier, this is what I would term a lie. Zig either tags its "untagged" unions, or they're not safe.

15 Likes

I gather from that Andrew is talking about how he increased the speed of the actual Zig compiler. By minimising cache misses. And how he did that by reorganising the compilers data into multiple arrays rather than and array of structs.

This is a well known thing. I believe they call it an "Entity Component System": Entity component system - Wikipedia

That is very good. Says more about the design of the compiler than the actual speed of Zig code. So far, the available bench mark test results indicate Rust and Zig are neck and neck. Rust VS Zig benchmarks, Which programming language or compiler is faster

There are plenty of people doing similar in Rust for their applications.

1 Like

I can't imagine many people would hassle you about using unsafe if you could prove it gave a 35% speed improvement. That seems like a no-brainer.

2 Likes

Some would. But most would accept it if you would provide safe interface. Which, in this particular case, is not that hard to achieve.

Perhaps there are even ready-made crate which provides such interface? It would be interesting for more things than just the compiler.

1 Like

I believe your opinion to be hyperbolic, unspecific and dismissive of the compentences of others.

I like Rust, but I have also programmed a lot of Java and always tried to make the best trade-offs possible.

This thread has veered quite off course from the original question, and I am personally sorry that I am adding to that.

That might be borderline. But I would be loath to allow unsafe into application level code for the sake of a small performance boost. Unless we were really desperate for that little gain.

In notice that Andrew seems to be talking about two different things that allowed the speed up. One being that if you have lots of objects scattered all around memory space on the heap that is going to be bad for the cache hit rate, where as putting them all together in an array, or splitting their parts over multiple arrays ECS fashion, is likely much better. The other being that tagged unions are likely bigger and hence hurt cache hoy rate. Who knows which one of those approaches made the biggest speed up?

So far I find it hard to believe that untagged unions, relying on some other means to tell what the data type in any blob of memory is, can be safe.

You are not alone. And I have seen many guys which achieved amazing results using Java. They would use various tricks to circumvent the fact that there are no unboxed structs and would do many things to make code fast.

But these are rare. Most Java developers are not thinking about it. At least they are not thinking about it before it's too late.

Seriously? Think Guice or Spring. Heck, even something like Freemarker.

You are, basically, replacing direct calls (which should execute half-dozen of maybe dozen CPU instructions) with a few hash lookups, reflection and good knows how many levels or indirection.

And, most important thing, last time I have looked that was the mainstream. Normal. Not something which you are supposed to be using when extreme flexibility is required, but how you write most of the code. Most programmers who are using C/C++ or Rust have looked on the output of the compiler, but how many Java programmers do you know who do that regularly?

Yes, there are Java programmers who do amazing things. I even know few personally.

But Java mainstream is similar to Python mainstream: hey, “premature optimization is root of all evil” thus let's design a baroque framework with insane number of indirection and XML and LDAP and JNDI and god knows what else and then, when you app uses 100 times more resources then it should start optimizing it.

And you are lucky if your application is just slow. Pretty often it contains “nice flexibility” which lead to big consequences.

The main issue with Java is not quaility of the language and the compiler (although these may be improved, and yes, I know about projects which try to improve them). Biggest issue is that culture which sprouts “make it work, make it right, make it fast” mantra. Guess what? Both step from “make it work” to “make it right” and step from “make it work” to “make it fast” require more-or-less total redesign and total rewrite. And who can afford to rewrite thing which is already “right”, hmm?

That is how we end up with the mess which we observe in most Java projects.

2 Likes