[HN Gopher] Speed of Rust vs. C
___________________________________________________________________
Speed of Rust vs. C
Author : sivizius
Score : 576 points
Date : 2021-03-13 03:51 UTC (19 hours ago)
(HTM) web link (kornel.ski)
(TXT) w3m dump (kornel.ski)
| AndyKelley wrote:
| > computed goto
|
| I did a deep dive into this topic lately when exploring whether
| to add a language feature to zig for this purpose. I found that,
| although finnicky, LLVM is able to generate the desired machine
| code if you give it a simple enough while loop continue
| expression[1]. So I think it's reasonable to not have a computed
| goto language feature.
|
| More details here, with lots of fun godbolt links:
| https://github.com/ziglang/zig/issues/8220
|
| [1]: https://godbolt.org/z/T3v881
| tiddles wrote:
| Thank you for doing the research and not just mindlessly adding
| features other languages have :)
| celeritascelery wrote:
| Really cool investigation. I wonder if this applies to rust as
| well.
|
| As you said though, this is finicky, and if you need this
| optimization for performance then you don't want to rely on
| compiler heuristics.
| Measter wrote:
| Rust's output[0] is basically the same as Zig in this case.
| The unsafe is needed here because it's calling extern
| functions.
|
| However, in this specific instance at least, this isn't as
| optimal as it could be. What this is basically doing is
| creating a jump table to find out which branch it should go
| down. But, because all the functions have the same signature,
| and each branch does the same thing, what it could have done
| instead is create a jump table for the function to call. At
| that point, all it would need to do is use the Inst's
| discriminant to index into the jump table.
|
| I'm not sure what it would look like in Zig, but it's not
| that hard to get that from Rust[1]. The drawback of doing it
| this way is that it now comes with the maintenance overhead
| of ensuring the order and length of the jump table exactly
| matches the enum, otherwise you get the wrong function being
| called, or an out-of-bounds panic. You also need to
| explicitly handle the End variant anyway because the called
| function can't return for its parent.
|
| I don't know Zig, but from what I understand it has some
| pretty nice code generation, so maybe that could help with
| keeping the array and enum in step here?
|
| [0] https://godbolt.org/z/sa6fGq
|
| [1] https://godbolt.org/z/P3cj31
| eru wrote:
| Somewhat off-topic: I just looked into zig, because you
| mentioned it.
|
| > C++, D, and Go have throw/catch exceptions, so foo() might
| throw an exception, and prevent bar() from being called. (Of
| course, even in Zig foo() could deadlock and prevent bar() from
| being called, but that can happen in any Turing-complete
| language.)
|
| Well, you could bite the bullet and carefully make Zig non-
| Turing complete. (Or at least put Turing-completeness behind an
| escape hatch marked 'unsafe'.)
|
| That's how Idris and Agda etc do it.
| skybrian wrote:
| With respect to deadlocks, there's little practical
| difference between an infinite loop and a loop that holds the
| lock for a very long time.
|
| Languages like Idris and Agda are different because sometimes
| code isn't executed at all. A proof may depend on knowing
| that some code will terminate _without running it_.
| anonymoushn wrote:
| Oh, that's great. I write interpreters off and on and I love
| Zig, so it's nice to hear I can get the best code gen while
| keeping the language small
| known wrote:
| https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
| shows C is generally better
| dthul wrote:
| Linking to a page that shows that the Rust version is faster
| than the C version in almost every case?
| Animats wrote:
| _" But the biggest potential is in ability to fearlessly
| parallelize majority of Rust code, even when the equivalent C
| code would be too risky to parallelize. In this aspect Rust is a
| much more mature language than C."_
|
| Yes. Today, I integrated two parts of a 3D graphics program. One
| refreshes the screen and lets you move the viewpoint around. The
| other loads new objects into the scene. Until today, all the
| objects were loaded, then the graphics window went live. Today, I
| made those operations run in parallel, so the window comes up
| with just the sky and ground, and over the next few seconds, the
| scene loads, visibly, without reducing the frame rate.
|
| This took about 10 lines of code changes in Rust. It worked the
| first time it compiled.
| adamnemecek wrote:
| I'm not sure how your architecture but you might not even need
| to lock things. I find that using mpsc channels allows me to
| get around like 60% of locking. Essentially, you have some sort
| of main loop, then you spawn a tread, load whatever you need
| there and then send it to the main thread over mpsc. The main
| thread handles it on the next iteration of the main loop.
| phkahler wrote:
| >> One refreshes the screen and lets you move the viewpoint
| around. The other loads new objects into the scene.
|
| How did you do that in Rust? Doesnt one of those have to own
| the scene at a time? Or is there a way to make that exclusive
| ownership more granular?
| Animats wrote:
| Since this got so many upvotes, I'll say a bit more. I'm
| writing a viewer for a virtual world. Think of this as a
| general-purpose MMO game client. It has no built-in game
| assets. Those are downloaded as needed. It's a big world, so
| as you move through the world, more assets are constantly
| being downloaded and faraway objects are being removed. The
| existing viewers are mostly single thread, in C++, and they
| run out of CPU time.
|
| I'm using Rend3, which is a 3D graphics library for Rust that
| uses Vulkan underneath. Rend3 takes care of memory allocation
| in the GPU, which Vulkan leaves to the caller, and it handles
| all the GPU communication. The Rend3 user has to create all
| the vertex buffers, normal buffers, texture maps, etc., and
| send them to Rend3 to be sent to the GPU. It's a light, safe
| abstraction over Vulkan.
|
| This is where Rust's move semantics ownership transfer helps.
| The thread that's creating object to be displayed makes up
| the big vertex buffers, etc., and then asks Rend3 to turn
| them into a "mesh object", "texture object", or "material
| object". That involves some locking in Rend3, mostly around
| GPU memory allocation. Then, the loader puts them together
| into an "object", and tells Rend3 to add it to the display
| list. This puts it on a work queue. At the beginning of the
| next frame, the render loop reads the work queue, adds and
| deletes items from the display list, and resumes drawing the
| scene.
|
| Locking is brief, just the microseconds needed for adding
| things to lists. The big objects are handed off across
| threads, not recopied. Adding objects does not slow down the
| frame rate. That's the trouble with the existing system.
| Redraw and new object processing were done in the same
| thread, and incoming updates stole time from the redraw
| cycle.
|
| If this was in C++, I'd be spending half my time in the
| debugger. In Rust, I haven't needed a debugger. My own code
| is 100% safe Rust.
| gridspy wrote:
| Wonderful! Thanks for sharing. This sounds like the exact
| sort of work that Rust is perfect for.
|
| I'm making a game in Rust and Godot (engine) and since it's
| a factory game the simulation performance is important.
| Rust means I worry far less about stability and
| performance.
|
| I bet if you wrote a good blog entry with screenshots and
| explanation of how your code loads and renders I imagine it
| would do well on HN.
| Animats wrote:
| Too soon. Someday perhaps a Game Developers Conference
| paper/talk. I was considering one, but live GDC has been
| cancelled for 2021. My real interest in this is how do we
| build a big, seamless metaverse that goes fast. I'm far
| enough along to see that it's possible, but not far
| enough along that people can use the client.
|
| Rust is good for this sort of thing. It's overkill for
| most web back end stuff. That's where Go is more useful.
| Go has all those well-used libraries for web back end
| tasks. Parallelism in web back ends tends to be about
| waiting for network events, not juggling heavy compute
| loads of coordinated disparate tasks. Hence all the
| interest in "async" for web servers. As I've said before,
| use the right tool for the job.
| gridspy wrote:
| One idea I liked was ...
|
| You have an authoritive world simulation server, as
| usual.
|
| You then have several servers whose chief job is to keep
| clients in sync with the authoritive server.
|
| Most network games combine these two roles, but there is
| a lot of processing and network traffic required to keep
| clients in sync. For massive multiplayer there is a
| benefit to scale the "client-interaction" servers.
| brink wrote:
| The simplest (and often best) option is to use the
| Arc<Mutex<MyStruct>> pattern.
|
| The Arc is an async reference counter that allows multiple
| ownership. And the nested Mutex enforces only one mutable
| borrow at a time.
| [deleted]
| efficax wrote:
| I think you mean that Arc is an atomic reference counter
| (it uses atomic cpu instructions to prevent race conditions
| when incrementing and decrementing the ref count)
| brink wrote:
| Ah yes, sorry. I remember it in my head with "async", but
| you're right. :)
| [deleted]
| ben0x539 wrote:
| Eh, with Arc you can share ownership easily, and there are
| probably a lot of cleverer concurrent data structures or
| entity component kinda things that'd just work too. But maybe
| you can arrange things so that one thread owns the scene but
| the other thread can still do useful work?
| ywei3410 wrote:
| This typically isn't possible because the rendering context
| is global and is needed for both loading and rendering. You
| need an Arc to guarantee the correct Drop mechanism.
| pmarin wrote:
| Without real world data _" fearlessly parallelizing all the
| things!"_ is an awful idea due to all the overhead involved.
|
| The most important design decision while writing a parallel
| algorithm is to decide for what amount of data is not worth it.
| riquito wrote:
| He tried with few effort and noticed that for his use case
| the code is faster, I fail to understand this rebuttal of the
| parent's comment
| fulafel wrote:
| As a general thought about parallelizing all the things
| it's true though. When looking for speedups,
| parallelization granularity has to be tuned and iterated
| with benchmarking, else your speedups will be poor or
| negative.
|
| I think the example case in this subthread was about making
| some long app operations asynchronous and overlapping,
| which is a more forgiving use case than trying to make a
| piece of code faster by utilizing multiple cores.
| [deleted]
| rbanffy wrote:
| The average cellphone today has more than 4 cores. A decent
| desktop can deal with 16 threads on 8 cores.
|
| There is a lot of untapped parallelism readily available
| waiting for the right code.
| PicassoCTs wrote:
| Parallelization is the nuclear energy of comp science.
| Loads of potential, high risk reward and they would have
| gotten away with it, if it were not for those meddling
| humans. Its non-trivial and can only be handled by
| accomplished engineers. Thus it is not used - or is
| encapsulated, out of sight, out of reach of meddling
| hands. (CPU Microcode shovelling non-connected work to
| pipelines comes to mind / NN-Net training frameworks,
| etc.)
| eru wrote:
| > [...] or is encapsulated, out of sight, out of reach of
| meddling hands.
|
| That's the real issue here! Most language have poor
| abstractions for parallelism and concurrency. (Many
| languages don't even differentiate between the two.)
|
| Encapsulating and abstracting is how we make things
| usable.
|
| Eg letting people roll hash tables by themselves every
| time they want to use one, would lead to people shooting
| themselves in the foot more often than not. Compared to
| that, Python's dicts are dead simple to use. Exactly
| because they move all the fiddly bits out of the reach of
| meddling hands.
| ShinTakuya wrote:
| It's not about number of available threads, the very act
| of scheduling tasks across multiple threads has
| scheduling and communication overheads, and in many
| situations actually ends up being slower than running it
| on the same thread.
|
| That said, I think the original comment was rightly
| pointing out how easy it was to make the change and test
| it, which in this case did turn out to be noticeably
| faster.
| scorpion95 wrote:
| Which form of Rust parallelism did you choose? I've read that
| the old one sucks.
|
| Certainly Rust is different beast than C.
|
| Code converted from C to Rust seems much more voluminous.
| Perhaps it's easier to maintain if you know Rust well?
|
| I cannot believe at first that Rust is ever more performant
| than C, as C could be made parallel and seems more barebones.
| One of the languages that CUDA can be used with is C, so I
| suspect that C may beat Rust in the world of computation for
| now? And Rust continues to evolve, I suspect, as C could.
|
| The author states:
|
| > My overall feeling is that if I could spend infinite time and
| effort, my C programs would be as fast or faster than Rust,
| because theoretically there's nothing C can't do that Rust can.
| But in practice C has less abstractions, primitive standard
| library, dreadful dependency situation, and I just don't have
| the time to reinvent the wheel, optimally, every time.
|
| So, I think for now: C is faster, even in parallel. But Rust
| could be nicer to use than C in many respects, so it may be
| worthwhile to consider it for large applications.
|
| That which we state definitively may be subjective, though.
| zesterer wrote:
| > My overall feeling is that if I could spend infinite time
| and effort, my C programs would be as fast or faster than
| Rust, because theoretically there's nothing C can't do that
| Rust can.
|
| The exact same argument applies to assembly code. There are
| very good reasons that it's not used nowadays except in
| incredibly rare circumstances or in the embedded world.
|
| It doesn't matter in the slightest how fast your language is
| _in theory_. Not one iota.
|
| The only thing that matters is how fast the programs you
| write with it are in practice. The evidence is clear: it is
| significantly easier to write faster programs in Rust than it
| is in C, and this applies to even the most skilled
| developers.
| bsder wrote:
| > Code converted from C to Rust seems much more voluminous.
|
| This is a fairly odd claim.
|
| If you have to deal with strings (ASCII and UTF-8) properly,
| C is stupidly verbose.
|
| If you need a data structure more complex than an array of
| something, C is stupidly verbose.
|
| If you want to deal with pattern matching/regexen, C is
| _ridiculously_ verbose.
|
| Do I agree that Rust is far more verbose for an embedded
| "blinky" (the embedded equivalent of "Hello, World!")? Yes.
|
| But once I start doing things like processing messages in a
| communications stack (BLE, CANOpen, Ethernet, etc.), Rust
| starts looking better and better.
| scorpion95 wrote:
| When I converted code from C to Rust, I was left with much
| more code.
|
| Are you just noting specifics, or was it your experience
| that there was less Rust required for a full C to Rust
| conversion?
| Jweb_Guru wrote:
| AFAIK it is a common experience that both C and C++ code
| tend to become fewer LOC in a move to Rust, a lot of it
| due to stuff like serde that greatly reduces boilerplate.
| It probably depends on your application and program size,
| though. I'm sure that if you're doing a lot of pointer
| wrangling or something it won't be smaller in Rust, while
| if your C program was just a bunch of SIMD intrinsics
| anyway like a lot of high performance code is now it'll
| probably be basically the same length.
| mcherm wrote:
| Did you ever take a look to try and figure out where the
| extra verboseness came from? For instance, was it spread
| equally across all functions or concentrated in those
| doing numeric computations (or whatever)? It would be
| interesting to learn something from this example.
| maccam94 wrote:
| Did you convert it by hand and translate the code into
| idiomatic rust? Or just run some C code through c2rust?
| Because the latter will not produce anything like a
| typical rust program, it will be C types and logic with
| minimal changes to make it compile in rust (and those
| changes will make it look very verbose).
| jsjohnst wrote:
| I mostly agree with your post but:
|
| > If you need a data structure more complex than an array
| of something, C is stupidly verbose.
|
| What's stupidly verbose about Structs to you?
| Volt wrote:
| "Data structure" here probably means something like a
| linked list, binary tree, or hash table.
| TazeTSchnitzel wrote:
| C being barebones does not mean it is faster. Because it has
| such weak typing and gives a huge amount of programmer
| freedom, compilers have to do a lot of work to be able to
| understand a C program well enough to optimise it.
|
| Rust, on the other hand, requires the programmer to give the
| compiler more information about what they're doing.
|
| A very simple example: void foobar(struct foo
| *f) { f->a += 2; foo(); f->a +=
| 2; bar(); f->a += 2; }
|
| Because C pointer types are so barebones, the compiler can't
| tell whether foo() and bar() can modify f->a just from
| looking at the above code. So it will always have to load and
| store that for each += operation.
|
| Rust on the other hand has two kinds of reference, rather
| than pointers: fn foobar(f : &mut foo) {
| f.a += 2; foo(); f.a += 2; bar();
| f.a += 2; }
|
| This is more high-level. But it's good for performance! Rust
| has a rule that you can only have one mutable reference to a
| struct at one time. Therefore, foo() and bar() can't be
| modifying f.a and it can simplify this to `f.a += 6;`.
|
| (You can see it in action for yourself here:
| https://godbolt.org/z/hWs67P. Sadly, Rust doesn't do this by
| default due to problems with LLVM, but eventually it can.)
| ragnese wrote:
| > Sadly, Rust doesn't do this by default due to problems
| with LLVM, but eventually it can.
|
| Can it, though? I keep hearing/reading FUD around both
| unsafe and Pin making it unlikely to ever be able to
| ubiquitously enable the noalias stuff.
| Jweb_Guru wrote:
| Code affected by unsafe already has to go through
| `UnsafeCell` which disables most aliasing optimizations,
| and the stacked borrow semantics have explicit opt-outs
| for such cases. I don't believe there are any Rust
| semantic issues standing in the way of exploiting
| aliasing information in this way, just LLVM bugs (and the
| fact that its restrict model isn't currently equipped to
| handle the information at such a fine granularity).
| nyanpasu64 wrote:
| There is undefined behavior in Rust affecting real-world
| code, including Tokio's scheduler, and code produced by
| async fn definitions. UnsafeCell doesn't solve the
| problem. There's more information at https://gist.github.
| com/Darksonn/1567538f56af1a8038ecc3c664a....
|
| Bug report at https://github.com/rust-
| lang/rust/issues/63818.
|
| Reddit threads at (older) https://www.reddit.com/r/rust/c
| omments/l4roqk/a_fix_for_the_... and (newer) https://www.
| reddit.com/r/rust/comments/lxw6cl/update_to_llvm....
|
| Somewhat related HN thread at
| https://news.ycombinator.com/item?id=26406989.
| Jweb_Guru wrote:
| This is the `Pin<&mut>` example which I'm not that
| familiar with but was aware of. I think it's highly
| unlikely that this one, relatively niche use case is
| going to prevent Rust from ever being able to safely turn
| on aliasing optimizations. There have been several
| solutions proposed, e.g. adding a stricter UnsafeCell to
| the language; they may technically not be backwards-
| compatible, but given how `Pin` is used and the fact that
| this is a soundness issue, I think it should be fine.
|
| The HN thread is mostly unrelated. I agree that it would
| have been better to integrate `Pin` directly into the
| language, though, but mostly for ergonomic reasons; it
| could still probably happen in an edition upgrade.
| kibwen wrote:
| You have correctly identified it as FUD. People have a
| bone to pick with Pin, so they irrationally latch on to
| it, but the general problem is the fact that the &mut
| invariants cannot currently permit any self-referential
| data, which is a useful concept in general (for intrusive
| data structures, etc) and whose lack that people have
| been hacking around since before 1.0, with crates like
| rental and owning_ref. The plan to fix this is to make
| self-referentiality a first-class concept in the
| language, as a principled exception to the usual &mut
| uniqueness invariant that preserves memory safety while
| properly encoding the aliasing guarantees.
| dataangel wrote:
| > The plan to fix this is to make self-referentiality a
| first-class concept in the language, as a principled
| exception to the usual &mut uniqueness invariant that
| preserves memory safety while properly encoding the
| aliasing guarantees.
|
| Are there issues I can subscribe to or RFCs for this?
| ameliaquining wrote:
| Source? I would like this to be true, but I haven't
| actually seen it anywhere.
| steveklabnik wrote:
| https://news.ycombinator.com/item?id=26410487 (it's a
| significant part, though not the entirety, of this
| comment)
| kimundi wrote:
| The github discussion thread where that originates from
| also contain the language designers optimistically
| discussing how in the worst case `Pin` just has to be
| hardcoded to exclude these optimizations. Which wouldn't
| make it the first type in the std lib that works similary
| (see interior mutability and UnsafeCell), so I personally
| intepret that as "unlikely to be an issue".
| cygx wrote:
| That's what _restrict_ is for: void
| foobar(struct foo *restrict f) { ... }
| TazeTSchnitzel wrote:
| Yes, C99 added that keyword, and you can use it in C
| code. (C++ is a more complicated matter, I believe...)
|
| But I think it's pretty uncommon in practice. One reason
| is that the C compiler has no borrow checker to help you
| notice when you're using `restrict` unsafely.
|
| (Also, there's the whole "strict aliasing" hell...)
| petschge wrote:
| It's not that uncommon in HPC codes. I.e. one of the
| places where speed matters.
| masklinn wrote:
| And just like that we're back to
|
| > There's a significant difference between what these
| languages can achieve in theory, and how they're used in
| practice.
|
| Theoretically in C you can use restrict for this.
| Practically nobody does (rust users keep finding bugs and
| miscompilation in LLVM's noalias support), and it's a
| huge footgun because you're completely on your own and
| all bets are off if you misuse it.
|
| Meanwhile in rust land, it's the default and the compiler
| checks your homework, and while you can always lie to the
| compiler it's much rarer that you'd even get in a
| position to do so.
| howinteresting wrote:
| But restrict is very hard to reason about correctly, and
| the consequences for making a mistake are potentially
| catastrophic.
|
| There's a very real difference between what's possible in
| theory and what humans do in practice. I wish Hacker News
| people engaged more with the practice.
| pdimitar wrote:
| Lately on another Rust thread somebody pointed out that C
| programs use a lot of indirection like pointers and vtable
| dispatches which actually detracts from the supposed mega-
| speed of the low-level C. I found that to be mind-blowing
| and felt stupid for not remembering that earlier.
| andrepd wrote:
| I don't think vtables are often used in C.
| howinteresting wrote:
| Dynamic dispatch (vtables are one way to do it, and so is
| a function pointer) is incredibly common in C, because
| there's no language-built-in way to perform
| monomorphization.
| codeflo wrote:
| I think they meant function pointers in general
| (callbacks and the like, see qsort). Also, a lot of C
| codebases end up implementing manual vtables of some sort
| where polymorphism is required.
| Lvl999Noob wrote:
| The author did say that there is nothing that Rust does that
| C cannot. The difference is that in Rust, those things are
| easier, or many times, the default way, while in C, you would
| have to take care of way too many things to make sure things
| work.
| scorpion95 wrote:
| Yes.
|
| But it's not just taking care of those things that must be
| done; it seems to bloat the code.
|
| I think it's a great alternative to C for large apps like
| Firefox, and perhaps it's a good alternative to Go for
| services.
|
| For general purpose, I personally want something fast
| that's easy, clear, and concise like Ruby.
|
| Do I just accept that Rust is the most evolved version of
| C, or is my gut correct that it's bloated? Is it a good
| choice for general purpose as-is?
| smt1 wrote:
| servo is written in rust, not firefox. The firefox
| codebase is quite old, while servo was written from
| scratch, being the first new web codebase in years.
| hderms wrote:
| As someone who's worked predominantly in high level
| languages (Scala, Ruby, etc...) I've found Rust to be
| relatively straightforward to use for simple CLI tools or
| gRPC servers (tonic library is pretty nice). I haven't
| tried building a CRUD app yet, but I don't see any real
| reason why it would be impossible to have an ergonomic
| web framework in Rust.
|
| The things I find most difficult: 1. Wrangling with the
| borrow checker can be painful before you know what you're
| doing (and even afterwards) but if you understand the
| standard library/patterns well, it seems to minimize the
| cost. Example being trying to write your own
| `get_or_else_insert` style method for a HashMap. Writing
| your own version is easy in other languages but hard in
| Rust. If you didn't know that methods like that already
| exist on HashMap you will experience a lot of pain until
| you understand the "right" way to do something.
|
| 2. Shared memory concurrency is definitely at the nexus
| of all the more difficult parts of Rust. Especially with
| async/await. There's no question in my mind that if you
| want to write a webserver that has async functions
| accessing shared memory, that you will for sure need to
| fill in any gaps in your knowledge as it will be
| difficult to get to a working program without
| understanding significantly more concepts than what it
| might take for a simple single-threaded CLI app
|
| I'm pretty sure that it will be possible (if it isn't
| already) to get the Rust ecosystem to a state where
| writing a CRUD app is about as simple as in Go (and
| considerably easier/more ergonomic w.r.t certain things
| like JSON serialization).
| anfilt wrote:
| Firefox's is compiled code is mostly written in C++ not
| C. You conflate C with C++, Java, and C#. C++ while has
| source compatibility with C tends to end up much
| different than C. C will not give you the OOP hell-scape
| you can dig yourself into with those three languages. C
| code tends be simpler and much more close to the assembly
| that will be generated than you would get in those
| language. Moreover, Java and C# are not even compiled.
| Anyways C != C++. C++ has changed quite a bit since it's
| earlier days and has diverged from plain C in a lot of
| ways. Some people even feel C++ keeps adding too many new
| features too fast.
| eru wrote:
| > Some people even feel C++ keeps adding too many new
| features too fast.
|
| It's not so much that they keep adding features, but that
| they (almost?) never remove any.
| pjmlp wrote:
| Python 3 has shown the world what happens when that is
| done without bringing the ecosystem along.
| anaerobicover wrote:
| Are there many other programming languages that remove
| features?
| ttt0 wrote:
| > C++ while has source compatibility with C
|
| It doesn't really, some things from C won't compile in
| C++, but that's a minor nitpick
| ben0x539 wrote:
| I guess people mostly take source compatibility to mean
| that you can write the headers for your C library so that
| it can be used from C++. That's not the same thing as C
| being a proper subset of C++ or whatever, but it's still
| a vast enough advantage of C++ over most competitors that
| it might as well be.
| DJBunnies wrote:
| Ruby is not fast.
| rbanffy wrote:
| I used to say that wrt to memory leaks with Java and C++
| and it remains true when comparing C and Rust:
|
| It's not that it's easier to write programs in [Java|Rust].
| It's that it makes it much harder to write the bugs.
| rocqua wrote:
| Yesterday I upgraded an entire code-base from C to C++
| just because it was faster than writing my own
| dynamically resizing array in C.
|
| Writing programs in C is harder just because there are
| essentially no containers in the stdlib.
| JohnStrangeII wrote:
| Aren't there good container libraries for C?
| cb321 wrote:
| https://github.com/glouw/ctl/
| rocqua wrote:
| They are either based on void * with performance issues,
| or macro based with weird ergonomics that look like
| function calls, but aren't.
|
| It's doable, but not very easy.
| cb321 wrote:
| You are right in a library-demographical sense, but not
| in a fundamental sense. There is a 3rd way. Have a look
| at the CTL I linked to (downvoted..maybe I should have
| explained more?).
|
| Once you give up the closed source/prebuilt binary
| library idea and embrace the C++-like header library idea
| and write implemenations in terms of "assumed
| macro/inline function" definitions, the problem becomes
| straightforward with no performance issue and different
| ergonomics issues than you probably think.
|
| It's a more "manual instantiation" than C++ templates or
| generics in other languages where just refering to them
| works, but most of C is quite manual. So, it fits the
| headspace & the hard parts of data structures/meddlesome
| hands remain factored out. Since you parameterize your
| files/code with #define/#include, you have to name your
| parameters which can make the instantiating client code
| more obvious than C++ templates with many arguments.
| OTOH, there is no/poor type checking of these parameters.
| rocqua wrote:
| I had a look, and it feels like template programming but
| with even worse guarantees.
|
| Having a type declaration dependent on #define P whether
| it is plain old data or not, and needing to know what
| that means, is not the kind of ergonomics I'd want. That
| requires learning a whole new paradigm to ensure I am not
| doing wrong things.
|
| In my mind it is so big an extension of the C language,
| that it leaves the C headspace and becomes its own
| headspace.
| ben0x539 wrote:
| There are a lot of great libraries for sure, but they
| aren't in the stdlib and C doesn't make it as easy to use
| external libraries as languages with modern tooling.
| Everybody gets grumpy about dependencies and a lot of
| people probably figure it's easier to maintain their own
| container code in their application than to deal with
| that.
| gridspy wrote:
| The very short version:
|
| There are many things which cause both frequent and rare
| strange crashes in multi-threaded code.
|
| In C, all these things will compile OK. In safe Rust,
| practically none of them will compile.
|
| It is much easier to fix compile issues in Rust to do with
| dangerous memory usage or thread sharing than it is to debug
| a compiled program.
| pjmlp wrote:
| Accessing files or database content from multiple threads
| without proper locking, or transactions, in place will
| compile just fine.
| orf wrote:
| What a silly point to make. The code you're writing in
| rust could launch a nuke that destroys the world and it
| will also compile just fine.
| bluejekyll wrote:
| Rust won't prevent all manner of bugs, but if you need to
| prevent multiple parts of your application from accessing
| a resource concurrently, it's fairly trivial to design
| types that would guarantee that at compile time.
|
| If you need to do it across processes, then you need a
| file system that supports exclusive file opens.
|
| The db question is a red-herring, because to properly
| solve for concurrency in the db, you need to use locks in
| the db (table and row locks), not the code accessing the
| db.
| Chilinot wrote:
| You dont necessarily need to use locks in the db. You can
| use transactions as well, unless you want to do major
| changes to the DB where transactions would be in constant
| conflict state.
| bluejekyll wrote:
| You're right, though that wasn't really the point of my
| reply. More that concurrency in the DB is a DB design
| choice and not the responsibility of the language
| accessing the DB. This is especially true when accessing
| the DB from multiple nodes, which most deployed software
| does.
| masklinn wrote:
| > Rust won't prevent all manner of bugs
|
| Nit: rust _will_ prevent all manner of bugs. Rust won 't
| prevent _every_ manner of bugs.
| pjmlp wrote:
| Indeed, but the fearless concurrency sales pitch tends to
| overlook that.
| ben0x539 wrote:
| The fearless concurrency sales pitch doesn't overpromise.
| You're trying to stretch it, but it's pretty clear on
| what kind of concurrency issues it covers (eg. data
| races).
| hderms wrote:
| Yeah it would be a grave misinterpretation of "fearless
| concurrency" to think Rust somehow validates that access
| to some shared resource with it's own semantics is also
| safe. I'm not educated on the subject but that problem
| seems pretty intractable for a language to solve in a
| general sense.
| pjmlp wrote:
| Indeed, yet that is not how many Rust advocacy blog posts
| sell it.
| trunnell wrote:
| It's hard to see how you could be arguing in good faith
| given that your point of "fearless concurrency" being an
| overstated "sales pitch" has now been answered with
| multiple substantive answers.
|
| Please stop moving the goal posts from "program
| concurrency" to "distributed or multi-process
| transactions." It subtracts from the conversation.
|
| Rust is honest about what it does and doesn't do.
| masklinn wrote:
| > Accessing [...] database content from multiple threads
| without proper locking, or transactions, in place will
| compile just fine.
|
| All the rust db interfaces I've seen so far forbid
| unlocked sharing of connections, statically (the
| connections are !Sync, in Rust speak).
| pjmlp wrote:
| They don't enforce the use of transactions, nor exclusive
| access to database tables.
| simias wrote:
| Rust has the expressive power to design an API that
| explicitly makes it impossible if you so desire though.
| For instance by using an API similar to mutexes where you
| have to lock to access the contents.
| rcxdude wrote:
| > Code converted from C to Rust seems much more voluminous.
| Perhaps it's easier to maintain if you know Rust well?
|
| My experience is mostly the opposite. For many tasks Rust can
| be much more concise (see anything with string handling, for
| example).
| alerighi wrote:
| Also Rust is risky to parallelize: you can get deadlocks.
|
| I don't get the obsession of parallel code in low level
| languages by the way. If you have an architecture where you can
| afford real parallelism you can afford higher level languages
| anyway.
|
| In embedded applications you don't usually have the possibility
| to have parallel code, and even in low level software (for
| example the classical UNIX utilities), for simplicity and
| solidity using a single thread is really fine.
|
| Threads also are not really as portable as they seem, different
| operating systems have different way to manage threads, or even
| don't supports thread at all.
| eru wrote:
| > In embedded applications you don't usually have the
| possibility to have parallel code, and even in low level
| software (for example the classical UNIX utilities), for
| simplicity and solidity using a single thread is really fine.
|
| Depends on which of the classic utilities you are talking
| about.
|
| Many of them are typically IO bound. You might not get much
| out of throwing more CPU at them.
| ben0x539 wrote:
| ripgrep? :)
| tsegratis wrote:
| ripgrep is based on re2, a c library
|
| I would guess it contains more c than rust code...
|
| But what I love about this article is its lack of hype.
| It makes clear arguments both ways and all of them I can
| get behind
|
| Hype doesn't help
|
| Edit: To all my downvoters; I anticipated you :) With
| love and best wishes
| carols10cents wrote:
| Why would you guess about how much C or Rust code that
| ripgrep contains when you could very quickly look?
| https://github.com/BurntSushi/ripgrep
| ben0x539 wrote:
| Hm, how do I go from the github repo to a language
| breakdown of the dependency tree?
| mplanchard wrote:
| I don't think people are downvoting you because they
| disagree on a matter of opinion. You've literally got the
| author of ripgrep having replied to you to tell you that
| what you've said is categorically false.
| tsegratis wrote:
| I anticipated my own falsity. I'm aware and at home with
| it
| burntsushi wrote:
| No it's not. Its regex library is written in Rust, but
| was _inspired_ by RE2. It shares no code with RE2. (And
| RE2 is a C++ library, not C.)
|
| Off the top of my head, the only C code in ripgrep is
| optional integration with PCRE2. In addition to whatever
| libc is being used on POSIX platforms. Everything else is
| pure Rust.
| tsegratis wrote:
| Ah, thanks burntsushi, I believe you are the ripgrep
| author even?
|
| Great work btw. Ripgrep is the best
|
| ... I will have to restrict my comment to just LLVM being
| a larger, c++, dependency
|
| ... Just angling for more downvotes ;) Thanks for the
| reply
| burntsushi wrote:
| To be clear, ripgrep has no runtime dependency on any
| LLVM or C++ library. rustc does.
| tsegratis wrote:
| The interesting thing here is that rust has good
| threading and fantastic crates
|
| I played with making a regex library in rust. Which, as
| per RE2 design involves constructing graphs and glueing
| them together as the regex is traversed
|
| This requires a cycle catching gc, or, just a
| preallocated arena... It was my first foray into rust and
| felt I would need to be hitting into unsafe, which I
| wasn't ready for. Array indexing might decompose into an
| arena, but syntactically just a bit messier (imho)
|
| Would be interesting to see how the RE2 does it in rust
| (didn't know that)
|
| I like how the article shows both sides of the fence, it
| makes me realize:
|
| I get a lot of optimizations from ptr stuffing in c. But
| sometimes we should lay down the good, for the better
| burntsushi wrote:
| You're overcomplicating it. When it comes to finite state
| machines at least, it's very easy to use an ID index
| instead of the raw pointer itself. That's exactly what
| the regex crate does.
|
| For reference, I am also the author of the regex crate.
| The only unsafe it uses specific to finite automata is to
| do explicit elimination of bounds checks in the core
| hybrid NFA/DFA loop.
| burntsushi wrote:
| Indeed. Many of the optimizations ripgrep (and the
| underlying regex engine) does only show benefits if the
| data you're searching is already in memory.[1] The same
| is true of GNU grep. This is because searching data
| that's in your OS's file cache is an exceptionally common
| case.
|
| [1] - I'm assuming commodity SSD in the range of a few
| hundred MB/s read speed. This will likely become less
| true as the prevalence of faster SSDs increases (low
| single digit GB/s).
| fulafel wrote:
| A lot of modern embedded hw are running operating systems
| providing threads (such as Linux) and multi-core CPUs.
| ReactiveJelly wrote:
| Deadlocks are unique to Rust, eh?
| burntsushi wrote:
| This is a bad take. ripgrep, to my knowledge, cannot be
| written in a higher level language without becoming a lot
| slower.[1] And yet, if I removed its use of parallelism by
| default, there will be a significantly degraded user
| experience by virtue of it being a lot slower.
|
| This isn't an "obsession." It's engineering.
|
| [1] - I make this claim loosely. Absence of evidence isn't
| evidence of absence and all that. But if I saw ripgrep
| implemented in, say, Python and it matched speed in the
| majority of cases, I would learn something.
| pjmlp wrote:
| Python isn't really something I would even think as
| possible example, Common Lisp, D, Nim, Swift, most likely.
| cb321 wrote:
| Cython with all the appropriate cdef type declarations
| can match C and so might also do it. Not sure Cython
| exactly counts as "Python"..it's more a superset/dialect
| { and I also doubt such a port would hold many lessons
| for @burntsushi, but it bore noting. }
| burntsushi wrote:
| So? I said, "higher level language." I didn't say,
| "Python specifically."
|
| I would guess D could do it.
|
| I don't know enough about Nim or Swift.
|
| I would learn something if Common Lisp did it. I'd also
| learn something if Haskell or Go did it.
| cb321 wrote:
| I am not trying to contradict anyone here, but any
| language mature enough to have an impl/way to not have
| arbitrary performance ceilings needs access to inline
| assembly/SIMD. Cython/Nim/SBCL can all do that..probably
| Haskell..Not so sure about Go or Swift. Anyway, _many_
| languages can respond well to optimization effort. I
| doubt anyone disagrees.
|
| At the point of realizing the above no ceiling bit, the
| argument devolves to more one about (fairly subjective)
| high/low levelness of the code itself/the effort applied
| to optimizing, not about the language the code is written
| in. So, it's not very informative and tends to go nowhere
| (EDIT: especially when the focus is on a single, highly
| optimized tool like `rg` as opposed to "broad demographic
| traits" of pools of developers, and "levelness" is often
| somewhat subjective, too).
| burntsushi wrote:
| You're missing the context I think. Look at what I was
| responding to in my initial message in this thread:
|
| > If you have an architecture where you can afford real
| parallelism you can afford higher level languages anyway.
|
| My response is, "no you can't, and here's an example."
|
| > but any language mature enough to have an impl/way to
| not have arbitrary performance ceilings needs access to
| inline assembly/SIMD
|
| If you ported ripgrep to Python and the vast majority of
| it was in C or Assembly, then I would say, "that's
| consistent with my claim: your port isn't in Python."
|
| My claim is likely more subtle than you might imagine.
| ripgrep has _many_ performance sensitive areas. It isn 't
| enough to, say, implement the regex engine in C and write
| some glue code around that. It won't be good enough. (Or
| at least, that's my claim. If I'm proven wrong, then as I
| said, I'd learn something.)
|
| > At the point of realizing the above no ceiling bit, the
| argument devolves to more one about (fairly subjective)
| high/low levelness of the code itself/the effort applied
| to optimizing, not about the language the code is written
| in. So, it's not very informative and tends to go
| nowhere.
|
| I agree that it's pretty subjective and wishy washy. But
| when someone goes around talking nonsense like "if
| parallelism is a benefit then you're fine with a higher
| level language," you kind of have to work with what you
| got. A good counter example to that nonsense is to show a
| program that is written is a "lower" level language that
| simultaneously benefits from parallelism and wouldn't be
| appropriate to do in a higher level language. I happen to
| have one of those in my back-pocket. :-) (xsv is another
| example. Compare it with csvkit, even though csvkit's CSV
| parser is written in C, it's still dog slow, because the
| code around the CSV parser matters.)
| cb321 wrote:
| Ok. "Afford parallelism => afford high level" with the
| implication of HL=slow does sound pretty off base. So,
| fair enough.
|
| FWIW, as per your subtle claim, it all seems pretty hot
| spot optimizable to me, at least if you include the
| memchr/utf8-regex engine in "hot spot". I do think the
| entire framing has much measurement vagueness ("hot",
| "vast majority", "levelness", and others) & is unlikely
| to be helpful, as explained. In terms of "evidence", I do
| not know of a competitor who has put the care into such a
| tool to even try to measure, though. { And I love rg.
| Many thanks and no offense at all was intended! }
| burntsushi wrote:
| ack might be an example. It's Perl, not Python, and its
| author is on record as saying that performance isn't his
| goal. So it's a bit of a strained one. But yes, it's
| true, I don't know any other _serious_ grep clone in a
| language like Python. This is why I hedged everything
| initially by saying that I know that absence of evidence
| isn 't evidence of absence. :-) And in particular, I
| framed this as, "I would learn something," rather than,
| "this is objective fact." So long as my standard is my
| own experience, the hand wavy aspect of this works a bit
| better IMO.
|
| > I do not know of a competitor who has put the care into
| such a tool to even try to measure, though.
|
| Right. Like for example, I am certain enough about my
| claim that I would never even attempt to do it in the
| first place. I would guess that others think the same.
| With that said, people have written grep's in Python and
| the like, and last time I checked, they were very slow.
| But yeah, the "development effort" angle of this likely
| makes such tools inappropriate for a serious comparison
| to support my claim. But then again, if I'm right, the
| development effort required to make a Python grep be as
| fast as ripgrep is insurmountable.
|
| > it all seems pretty hot spot optimizable to me
|
| As long as we're okay with being hand wavy, then I would
| say that it's unlikely. Many of the optimizations in
| ripgrep have to do with amortizing allocation, and that
| kind of optimization is just nearly completely absent in
| a language like Python unless you drop down into C. This
| amortization principle is pervasive and applies as deep
| as regex internals to the code the simply prints
| ripgrep's output (which is in and of itself a complex
| beast and quite performance sensitive in workloads with
| lots of matches), and oodles of stuff inbetween.
|
| > { And I love rg. Many thanks and no offense at all was
| intended! }
|
| :-) No offense taken. This is by far the best convo I'm
| having in this HN post. Lol.
|
| Note that I've made similar claims before. In the last
| one, there is a lot more data:
| https://news.ycombinator.com/item?id=17943509
| cb321 wrote:
| When I used to write in Cython + NumPy I would pre-
| allocate numpy arrays written into by Cython. It's
| C-like, but because of the gradual typing I think firmly
| in the high _er_ level (for some value of "er"). One can
| certainly do that stuff in Nim/SBCL/etc. (and one sees it
| done).
|
| While allocation is pretty pervasive, I'm skeptical that
| everywhere or even most places you do it is an important
| perf bottleneck. Without a count of these 20 times it
| matters and these 40 it doesn't, it's just kind guesswork
| from an all too often frail human memory/attention that
| "ignores the noise" by its very nature. You might be
| right. Just trying to add some color. :-)
|
| Another way to think of this is to imagine your own
| codebase "in reverse". "If I drop this optim, would I see
| it on that profile?" Or look at the biggest piles of code
| in your repo and ask "Is this in the critical path/really
| perf necessary?" and the like. Under the assumption that
| high _er_ level things would be a lot shorter that kind
| of thought experiment can inform. Maybe an approach
| toward more objectivity, anyway. Little strewn about
| tidbits in every module don 't really count { to me :-) }
| - that speaks more to abstraction problems.
|
| But I don't think there is a lot of value in all the
| above gendankenizing. While I realize some bad "just
| throw money at it" kicked this off, one of my big
| objections to the entire framing is that I think people
| and their APIs really "alter the level" of a language.
| Indeed their experience with the language has big impact
| there. Every one reading this knows C's `printf(fmt,
| arg1, arg2,..)`. Yet, I'd bet under 1% have heard
| of/thought to do an allocating (or preallocated) string
| builder variant like `str(sub1, sub2, ..., NULL)` or
| using/acquiring something like glibc's `asprintf`. People
| will say "C is low level - It has no string
| concatenation!". Yet, inside of an hour or two most
| medium-skill devs could write my above variadic string
| builder or learn about vasprintf. Or learn about Boehm-
| Wiser for garbage collected C or 100 other things like
| that CTL I mentioned elsewhere in this thread.
|
| So what "level" is C, the language? Beats me. Does it
| have concatenation? Well, not spelled "a+b" but maybe
| spelled not much worse "str(a,b,NULL)". Level all just
| depends so much on how you use it. Performance is
| similar. Much C++ (and Rust for that matter) is terribly
| inefficient _because_ of reputations for being "fast
| languages" leading to less care (or maybe just being done
| by junior devs..). These "depends" carry over to almost
| anything..not just Rust or C, but sometimes even English.
| I am usually told I write in much too detailed a way and
| a trimmer way might have higher persuasion/communication
| performance! { How's that for "meta"? ;-) }
|
| > This is by far the best convo I'm having in this HN
| post. Lol.
|
| Cool, cool. There can be a lot of "Rust Rage" out there
| (in both directions, probably). :)
|
| Anyway, I don't think we'll resolve anything objective
| here, but don't take a lack of response as indicating
| anything other than that. You aren't making any strong
| objective claims to really rebut and I'm glad that you
| personally undertook the challenge to do ripgrep in any
| language. I do think many might have done..maybe Ada,
| too, and probably many more, but maybe all at the same
| "realized levelness". You just did not know them/feel
| confident about getting peformance in them. Which is
| fine. A.Ok, even! I guess your other biggy is Go and that
| might actually not have worked of all the alternatives
| bandied about by pjmlp and myself so far.
| yholio wrote:
| You would go to parallelism precisely on those platforms
| where simpler performance fixes (changing some data
| structures or implementing limited sections in a fast
| language) are insuficient. Eficient parallelization of an
| existing algorithm is a major undertaking.
| amelius wrote:
| But Rust works badly with mmapped (memory-mapped) files, as the
| article notes. So in C you could load (and save!) stuff almost
| instantly, whereas in Rust you still have to de-serialize the
| input stream.
| amluto wrote:
| In C you can access pointers to memory mapped files
| effortlessly in ways that are often extremely unsafe against
| the possible existence of other writers and against the
| making being unmapped and mapped elsewhere. It's also
| traditional to pretend that putting types like int in a
| mapped file is reasonable, whereas one ought to actually
| store bytes and convert as needed. Rust at least requires a
| degree of honesty.
| burntsushi wrote:
| No you don't. I've written multiple programs that load things
| instantly off the file system via memory maps. See the fst
| crate[1], for example, which is designed to work with memory
| maps. imdb-rename[2] is a program I wrote that builds a
| simple IR index on your file system that can then instantly
| search it by virtue of memory maps.
|
| Rust "works badly with memory mapped files" doesn't mean,
| "Rust can't use memory mapped files." It means, "it is
| difficult to reconcile Rust's safety story with memory maps."
| ripgrep for example uses memory maps because they are faster
| sometimes, and its safety contract[3] is a bit strained. But
| it works.
|
| [1] - https://github.com/BurntSushi/fst/
|
| [2] - https://github.com/BurntSushi/imdb-rename
|
| [3] - https://docs.rs/grep-
| searcher/0.1.7/grep_searcher/struct.Mma...
| amelius wrote:
| I didn't read your code but one problem I suspect you ran
| into is that you had to re-invent your container data
| structures to make them work in a mmapped context.
| burntsushi wrote:
| No, I didn't. An fst is a compressed data structure,
| which means you use it in its compressed form without
| decompressing it first. If you ported the fst crate to C,
| it would use the same technique.
|
| And in C, you have to design your data structures to be
| mmap friendly anyway. Same deal in Rust.
|
| But this is moving the goal posts. This thread started
| with "you can't do this." But you can. And I have.
| Multiple times. And I showed you how.
| amelius wrote:
| > which means you use it in its compressed form without
| decompressing it first.
|
| So your code operates directly on a block of raw bytes? I
| can see how that can work with mmap without much
| problems.
|
| My argument was more about structured data (created using
| the type system), which is a level higher than raw bytes.
| burntsushi wrote:
| > So your code operates directly on a block of raw bytes?
| I can see how that can work with mmap without much
| problems.
|
| Correct. It's a finite state machine. The docs of the
| crate give links to papers if you want to drill down.
|
| > My argument was more about structured data (created
| using the type system), which is a level higher than raw
| bytes.
|
| Yes. You should be able to do in Rust whatever you would
| do in C. You can tag your types with `repr(C)` to get a
| consistent memory layout equivalent to whatever C does.
| But when you memory map stuff like this, you need to take
| at least all the same precautions as you would in C. That
| is, you need to build your data structures to be mmap
| friendly. The most obvious thing that is problematic for
| mmap structures like this that is otherwise easy to do is
| pointer indirection.
|
| With that said, this technique is not common in Rust
| because it requires `unsafe` to do it. And when you use
| `unsafe`, you want to be sure that it's justified.
|
| This is all really besides the point. You'd have the same
| problems if you read a file into heap memory. The main
| problem in Rust land with memory maps is that they don't
| fit into Rust's safety story in an obvious way. But this
| in and of itself doesn't make them inaccessible to you.
| It just makes it harder to reason about safety.
| morty_s wrote:
| Dang, burntsushi up in the house! Hey, just wanted to say
| I enjoy your work--I've learned a lot from it. Thank you!
| fractionalhare wrote:
| It's very tedious to debate with someone who explicitly
| makes assumptions about something (like code) without
| having read it, and puts the burden of refuting those
| assumptions on you...
| bluejekyll wrote:
| It doesn't say it "works badly" it says the borrow checker
| can't protect against external modifications to the file
| while memory-mapped, which has a host of issues in C as well.
|
| You can mmap files in Rust just fine, but it's generally as
| dangerous as it is in C.
| bakatubas wrote:
| I don't get this obsession with "dangerous." Honestly, what
| does that even mean? I think a better word is "error-
| prone." Danger is more like, "oh my god a crocodile!"
| kortex wrote:
| Concurrency bugs can absolutely cause dangerous danger of
| the deadly variety:
|
| https://en.m.wikipedia.org/wiki/Therac-25
| drchickensalad wrote:
| Errors are up to interpretation. It just means the thing
| didn't happen as requested. Errors are meant to be
| expected or not expected depending on the context.
|
| Dangerous means dangerous. It's not up for
| interpretation.
|
| Languages have multiple, very different words, for
| exactly this reason.
| steveklabnik wrote:
| > Honestly, what does that even mean?
|
| It has a very specific meaning in Rust: the user can
| cause memory unsafety if they make a mistake.
|
| > I think a better word is "error-prone."
|
| The issue with the connotation there is that it's not
| about the rate of problems, it's about them going from
| "impossible" to "possible."
| amelius wrote:
| There can be real danger when the code is used in certain
| applications. For example when controlling the gate of
| the crocodile cage in a zoo.
| amelius wrote:
| But that may be of little solace. If you snapshot your
| entire heap into an mmapped file for fast I/O, then
| basically the entire advantage of Rust is gone.
| bluejekyll wrote:
| Is there literally no other code in the application?
|
| Rust has plenty of situations where you do unsafe things
| but wrap that in safe APIs. If you're returning regions
| of that mmapped file, for example, a lifetime can be
| associated to those references to ensure that those are
| valid for the duration of the file being mmapped in the
| program.
|
| It can be used to ensure that if you need to write back
| to that mmapped file (inside the same program) that there
| are no existing references to it, because those would be
| invalid after an update to the file. You need to do the
| same in C, but there are no guardrails you can build in C
| to make that same assurance.
| ReactiveJelly wrote:
| > If you snapshot your entire heap into an mmapped file
| for fast I/O,
|
| I've never heard of this trick. And my first reaction is
| "That would be a nightmare of memory unsafety if I did it
| in C++"
|
| What's it used for? IPC?
| amelius wrote:
| You have to combine it with other techniques, e.g.
| journaling to make it safe, but this is not always
| necessary (e.g. when using large read-only data-
| structures)
| howinteresting wrote:
| I'd call mmaping data structures into memory an advanced
| systems programming trick which can result in a nice
| performance boost but which also has some severe
| drawbacks (portability across big/little endian
| architectures and internal pointers being two examples).
|
| I know some very skilled C++ and Rust developers who can
| pull it off. If you're at that skill level, Rust is not
| going to get in your way because you're just going to use
| unsafe and throw some sanitizers and fuzzers at it. I
| wouldn't trust myself to implement it.
| loeg wrote:
| I think emacs (used to?) do something awful like this.
| https://lwn.net/Articles/707615/
| high_density wrote:
| is it something deeply ingrained to rust? or is it something
| rust is working on?
| JulianMorrison wrote:
| It's more like, Rust wants to make guarantees that _just
| aren 't possible_ for a block of memory that represents a
| world-writable file that any part of your process, or any
| other process in the OS, might decide to change on a whim.
|
| In other words, mmaped files are _hard_ , and Rust points
| this out. C just provides you with the footgun.
| codeflo wrote:
| The problem is that compilers are allowed to make some
| general assumption about how they're allowed to reorder
| code, always based on the assumption that no other process
| is modifying the memory. For example, the optimizer may
| remove redundant reads. That's a problem if the read isn't
| really redundant -- if the pointer isn't targeting process-
| owned memory, but a memory mapped file that's modified by
| someone else. Programs might crash in very "interesting"
| ways depending on optimization flags.
|
| C has this issue as well, but Rust's compiler/borrow
| checker is particularly strong at this kind of analysis, so
| it's potentially bitten even harder.
| pjmlp wrote:
| While it works great for some cases, one should not forget it
| doesn't cover external resources, specially those shared across
| processes.
| bluejekyll wrote:
| You have made this claim multiple times. Why do you see this
| as a language issue and not an OS issue? It becomes an even
| bigger problem when we talk about distributed systems and
| distributed resources. Is there a language that handles this?
|
| These issues about multiple processes and distributed systems
| are framework and OS level concerns. Rust helps you build
| fast concurrent solutions to those problems, but you're
| correct that it can not solve problems exterior to the
| application runtime. How is that a deficiency with Rust?
| fulafel wrote:
| How could an OS adapt its processes functionality to help
| Rust here?
| ben0x539 wrote:
| Capabilities would probably be helpful :V
| acje wrote:
| https://wasmcloud.dev/
| pjmlp wrote:
| Fearless concurrency sales pitch.
|
| Yes languages like Erlang and runtimes like Coyote and
| Orleans.
| bluejekyll wrote:
| Erlang has a great concurrency model with higher overhead
| than Rust, but similar cross thread safety, doesn't do
| anything about exterior resources to the application.
|
| I've not worked with Coyote, but if it is the system for
| .net, it describes itself as a framework, "Coyote
| provides developers a programming framework for
| confidently building reliable asynchronous software on
| the .NET platform".
|
| Orleans similarly describes itself as a framework,
| "Orleans is a cross-platform software framework for
| building scalable and robust distributed interactive
| applications based on the .NET Framework."
|
| Rust is a language, similar frameworks are being built
| with it, the point your making does not appear to be
| about the language.
| zelphirkalt wrote:
| If I understand correctly, the Erlang point was, that you
| can have a distributed system by using Beam to scale to
| multiple machines and have them communicate via message
| passing, which is all possible and encouraged, because of
| how you structure and write code in Erlang, as actors
| with mailboxes, isolating actors from each other, except
| for the messages, that are passed.
|
| You say, that the Erlang concurrency model has higher
| overhead than Rust. In Rust there are probably multiple
| projects going on right now (one of them is Bastion, but
| I guess there are probably others), which try to provide
| Erlang like concurrency. What do you mean by overhead of
| a concurrency model (that of Erlang) being higher than
| the overhead a programming language (Rust)? As far as I
| know Erlang's lightweight processes are about as
| lightweight as you can get. Is there a Rust framework for
| Erlang like concurrency, which reduces the footprint of
| lightweight processes even more?
| bluejekyll wrote:
| That wasn't meant to be a snide comment about Erlang in
| any way. All I meant by the comment about higher-overhead
| was that the language itself generally has more costs to
| run, i.e. runtime, memory usage, garbage collector,
| interpreted, etc, than Rust.
|
| The "process" model of Erlang is about as lightweight as
| you can get, agreed.
|
| In terms of capabilities of beam across systems, point
| taken. Though we start stretching some of the
| understanding of where languages end and runtimes
| begin... Rust and C make those boundaries a little more
| clear.
| mratsim wrote:
| > There are other kinds of concurrency bugs, such as poor use of
| locking primitives causing higher-level logical race conditions
| or deadlocks, and Rust can't eliminate them, but they're usually
| easier to diagnose and fix.
|
| Which is why so many people are creating formal verification
| languages and spending years in research to fix those ... That
| just isn't true. It's a very complex problem that is an issue in
| both hardware (cache-coherency protocols) to OS (atomics locks)
| to higher level construct (commit-rollback in databases).
|
| Consequently
|
| > But the biggest potential is in ability to fearlessly
| parallelize majority of Rust code, even when the equivalent C
| code would be too risky to parallelize. In this aspect Rust is a
| much more mature language than C.
|
| This couldn't be more wrong either. Rust doesn't help you write
| synchronization primitives safely because it doesn't handle
| synchronization like locks, condition variables or atomics. You
| need formal verification to be fearless.
| zesterer wrote:
| You've just taken the word 'fearless', a word that's clearly
| subjective, and said that the definition the author gives of it
| "couldn't be more wrong". That's... a choice.
| mratsim wrote:
| The word is misrepresenting the problem of synchronization
| and reducing to only memory safety.
|
| If it was that simple, Tokio wouldn't need to formally verify
| their implementation with an external tool and it wouldn't
| have found dozens of well hidden bugs.
| ben0x539 wrote:
| Rust may or may not help you write synchronization primitives
| safely, but it for sure helps you use synchronization
| primitives without having to worry about memory safety. If you
| aren't parallelizing particularly subtle shenanigans, that's
| plenty for fearlessness.
| mratsim wrote:
| Coming up with a new threadsafe queue design is worthy of a
| paper even though it's just enqueueing and dequeueing items.
|
| Memory safety is just a small part and is a much easier
| problem than ensuring the absence of race conditions.
| zesterer wrote:
| Code 'bloat' is a bizarre metric to use for anything unless
| you're on a platform with incredibly constrained executable
| memory like an embedded device.
|
| The fact that Rust specialises its generic code according to the
| type it's used with it not some inherent disadvantage of
| generics. That's what they're _supposed_ to do. By choosing to
| not specialise, you 're actively making the decision to make your
| code _slower_. Rust has mechanisms for avoiding generic
| specialisation. They 're called trait objects and they work
| brilliantly.
|
| When you use void* in your data structures in C, you're not
| winning anything when compared to Rust. You're just producing
| slower code that mimics the behaviour of Rust's trait objects,
| but more dangerously.
|
| Code 'bloat' (otherwise known as 'specialising your code
| correctly to make it run faster') is not a reason to not use Rust
| in 2021, so please stop pretending that it is.
| Tuna-Fish wrote:
| It's not that simple. While fully specializing everything wins
| microbenchmarks, as C++ has shown time and time again, it can
| easily lose performance in large applications. If fully
| specializing code saves a few branches in the hot loop, but
| also blows through all the L1i, it can easily be a huge net
| negative.
|
| > Rust has mechanisms for avoiding generic specialisation.
| They're called trait objects and they work brilliantly.
|
| As someone who uses a lot of rust, they are sort of the red-
| headed stepchild. As a minimum to make the properly usable, we
| need a way of passing one object with multiple different
| traits.
| loeg wrote:
| > As someone who uses a lot of rust, they are sort of the
| red-headed stepchild. As a minimum to make the properly
| usable, we need a way of passing one object with multiple
| different traits.
|
| What do you mean? fn foo<T: TraitA +
| TraitB>(x: T) { T.something(); }
| Tuna-Fish wrote:
| Unless TraitB is an auto trait, that isn't currently valid?
|
| From the reference:
|
| > Trait objects are written as the optional keyword dyn
| followed by a set of trait bounds, but with the following
| restrictions on the trait bounds. All traits except the
| first trait must be auto traits, there may not be more than
| one lifetime, and opt-out bounds (e.g. ?Sized) are not
| allowed.
|
| The only one of those restrictions that is acceptable to
| have is the single lifetime one. All the others are
| seriously restricting. The devs seem to agree, but work on
| this aspect of rust is very slow, and people are arguing on
| how to implement it. (I, for one, feel very strongly that
| dyn TraitA + TraitB should have a size of 3 pointers. That
| is, no magic combining vtables, just every added trait adds
| a new pointer to vtable.)
| steveklabnik wrote:
| This is not a trait object, and so that part of the
| reference is irrelevant.
|
| Here's that example working: https://play.rust-
| lang.org/?version=stable&mode=debug&editio...
|
| I am not entirely sure that it answers your point though;
| what you want is trait A _or_ trait B, not both, right?
| Tuna-Fish wrote:
| Ah sorry, no, what I want is specifically a trait object
| that satisfies both trait A and B.
| steveklabnik wrote:
| Yes, in that case you are 100% right, your parent didn't
| provide an actual solution.
| zesterer wrote:
| > As a minimum to make the properly usable, we need a way of
| passing one object with multiple different traits.
|
| Supertraits?
| Tuna-Fish wrote:
| That is possible, but gets really hairy if you use a lot of
| trait objects.
| 12thwonder wrote:
| I prefer to have great ideas in rust ported over to C instead of
| rewriting everything with Rust. this approach will benefit all
| the existing softwares written in C which I think is much larger
| than Rust in terms of both impact and code size.
|
| am I a minority having this opinion?
| pornel wrote:
| This is a popular sentiment. However, there's Checked-C and
| Cyclone, and they have very little traction.
|
| To make static analysis robust in C you need to start reliably
| tracking ownership and forbid type-erasing constructs. This
| typically means adding smart pointers, some kind of borrow
| checking or garbage collection, generics to replace void*,
| maybe tagged unions, and a new standard library that embraces
| these features.
|
| It's going to bring most of Rust's complexity and require major
| code changes anyway, but you won't even get benefits of a newer
| language.
| varajelle wrote:
| C with generics and destructors and containers in the standard
| library. We could call that language C++.
| ben0x539 wrote:
| I don't know if you are a minority, but Rust is available right
| now and C-but-with-Rust's-great-ideas isn't. As far as I know
| no one is working on C-but-with-Rust's-great-ideas, so I don't
| think it's a good strategy to wait around for it instead of
| using the tools that exist and are already used with great
| impact.
| 12thwonder wrote:
| for new projects, sure. but when it comes to existing c/c++
| projects, I'm not a big fan of rewriting everything.
| ben0x539 wrote:
| I mean, it's probably infeasible to rewrite everything and
| it makes sense to focus on the cases where it'd have the
| greatest impact, sure.
| paavohtl wrote:
| It's a nice thought, but generally speaking you can't port
| these ideas over to C without effectively creating a new
| backwards incompatible language.
| Jweb_Guru wrote:
| I think it would be basically impossible to perform this task
| without making the language fundamentally not C. Zig is an
| interesting take in that direction (learn from the last 30
| years but still try to be "C") that I think gets a lot closer
| to the ideal than most other alternatives.
|
| C++, OTOH, you could probably port most of Rust's concepts into
| (with some extra language changes for various reasons I don't
| want to get into). However, since almost no existing C++ code
| would typecheck in the "safe" subset without modifications, it
| would effectively be a different language anyway. And to be
| clear, this isn't necessarily because people are routinely
| doing dangerous stuff in C++ -- the whole Rust ecosystem has
| grown up around the borrow checker, which means some very basic
| things people use in most other languages aren't done. Here are
| some examples of things typical Rust code does differently from
| typical C++ code due to it making it much harder to perform
| safety checks, beyond the obvious aspect of lifetime
| annotations and genuinely unsafe patterns like accessing
| globals (sorry, it just is):
|
| * far less use of accessors, especially mutable ones (because
| Rust can't track split field ownership)
|
| * Rust tends to split up big "shared context" structures
| depending on function use, rather than logical relationships,
| for much the same reason (Rust conservatively assumes that all
| fields are used when a context object gets passed to a function
| as long as any pointer to the structure remains, even if the
| fields you use aren't being accessed).
|
| * Rust almost never uses internal or cyclic pointers. It's safe
| to do it with boxed data or data that doesn't move, and there
| are safe type mechanisms around that, but it's cumbersome since
| it has to be visible to the typechecker, so people usually
| don't bother.
|
| * single-threaded mutation through multiple pointers into the
| same data structure, which may even be aliased. Again, often
| safe (though not always), and in the safe cases there are
| generally safe types to enable it in Rust, but since it's not
| the default and requires pre-planning for all but the simplest
| cases, people usually don't bother.
|
| * Rust types are _always_ annotated with thread safety
| information. This is usually done by default, but if it weren
| 't it would be a huge amount of boilerplate. The reason this
| works is that in the cases where people are doing unsafe stuff,
| the type system automatically opts out and requires them to opt
| in. Libraries have been built around this assumption. Even if
| we were to port such a mechanism over to C++, the lack of these
| explicit annotations would mean that in practice it just
| wouldn't work that well--you would have to do a very detailed
| thread safety analysis of basically any existing library to try
| to assign types.
|
| Often, complying with these kinds of rules is what people
| coming to Rust struggle with--not so much local lifetime issues
| which the compiler can usually figure out, but how to structure
| the entire program to make life easy for the borrow checker.
| However, complying comes with a big benefit--it allows safety
| analysis to proceed purely locally in almost all cases. The
| reason that static analyzers don't just "do what Rust does" is
| that they're dealing with programs that aren't structured that
| way and need to perform far more global analysis to catch most
| of the interesting memory safety bugs that pop up in mature C++
| codebases, especially the ones that evade code review.
|
| So--do I think it would be great to port this stuff over to C++
| (or C, hypothetically?). Absolutely--I still prefer Rust as a
| language, but at the end of the day memory safety you could
| layer on top of existing C code would be a huge win for
| everyone. But I don't see it happening because of the fact that
| Rust's solution requires serious code restructuring. if people
| are going to have to rewrite their old programs anyway to work
| with a tractable static analysis, and not be able to use almost
| any existing libraries, it's not clear how much more benefit
| they'd have from using this subset than from just switching to
| Rust.
| 12thwonder wrote:
| I do agree with most of your points, porting may not be
| possible. However, I was just wondering if the future of
| C/C++ can be much safer than it is right now. for example,
| GCC's GUARDED_BY macro is a big help in thread safety for
| c/c++. not sure how much further we can go but just a
| thought.
| hsaliak wrote:
| For parallelism, Modern tooling like TSAN can close the gap
| somewhat. If you are planning to introduce threads, not testing
| it with TSAN is silly at best.
| howinteresting wrote:
| If you're writing safe, parallel Rust code, you don't really
| need to use TSAN. You may hit a deadlock sometimes, but those
| tend to be easy to figure out in my experience.
|
| The people implementing the libraries you use (e.g. Rayon) may
| have to use TSAN, of course.
| hsaliak wrote:
| For sure - I was mentioning TSAN in the context of threaded C
| code
| planetis wrote:
| Its just amusing, in this thread everyone with critical thinking
| and skeptical is down voted, even if one expresses himself
| moderately. It shows how much of zealots, Rust fanboys have
| become.
| fractionalhare wrote:
| Can you point to some specific comments like this? None of the
| top threads seem to show this, as of this writing it's mostly
| about thread versus process parallelism and which kinds of
| conditions require unsafe.
| [deleted]
| junippor wrote:
| Not parent, but to me this one stood out.
|
| https://news.ycombinator.com/item?id=26445167
| cb321 wrote:
| And now one of mine
| https://news.ycombinator.com/item?id=26448822 in a subthread
| where the other main commentor says the subthread is his
| favorite of this whole thread. There just might be something
| to this downvoting claim..
| cb321 wrote:
| Several posts by @pjmlp..(if they stay downvoted).
| howinteresting wrote:
| Bothsidesism is unhelpful in technical discussions just as much
| as in politics. If you have _specific_ critiques please share
| them.
|
| I have a number of _specific_ critiques of Rust, chief being
| that APIs and implementations are bound too tightly. &[String]
| and &[&str] are logically similar but changing from one to the
| other in your implementation might mean a breaking API change.
| [deleted]
| hctaw wrote:
| fn func (slice: &[impl AsRef<str>]) { // ...
| }
| howinteresting wrote:
| * You have to remember to do that.
|
| * I was thinking return values.
|
| * Also you can't use that style in an enum definition if
| you want to return a custom enum.
| hctaw wrote:
| If you need api flexibility you use generics, and that is
| the way to be generic over types that refer to strs. I'm
| pretty sure this is in the book, and it's common enough
| that even someone who doesn't use rust full time (myself)
| knows it off the top of their head
|
| You can use impl Trait in returns, this is actually one
| of the reasons why that feature exists.
|
| And yes, you can use generics in an enum.
| howinteresting wrote:
| You have to expose the generic in the enum though. Rust
| doesn't have existential types at the moment.
| up2isomorphism wrote:
| I think it would a be very in interesting psychological study
| on the reason for this. The similar thing happens for some
| other languages, but never at the level of rust.
| ben0x539 wrote:
| Hm, is there some specific criticism of Rust you'd like to see
| discussed more? It's easy to get side-tracked in these
| "actually my language is better than your language" with
| everybody launching whole broadsides of arguments, so I
| wouldn't be surprised if some more subtle points get lost.
| planetis wrote:
| eh, I don't partake in the usual "actually my language is
| better than your language" that comes up in all posts. I just
| don't like people going overboard with their claims, when
| they try to promote any PL really, and would appreciate more
| fact checking.
| jedisct1 wrote:
| This is the case every time there's a post about Rust.
| nindalf wrote:
| The problem is that most people criticising Rust don't make the
| case very well. If you want to read good critique, I'd
| recommend this - https://matklad.github.io/2020/09/20/why-not-
| rust.html. This post up-to-date, succinct and objective.
|
| And most pertinently, this critique was written by someone who
| genuinely loves programming in Rust. Shows you that Rust users
| aren't blinded to the faults of the language. You shouldn't
| think that Rust users are fanboys just because you see push
| back to low effort, low knowledge critiques.
| cambalache wrote:
| > And most pertinently, this critique was written by someone
| who genuinely loves programming in Rust.
|
| That is putting the bar impossible high. I would expect most
| of the criticism to come from people who hate to program in
| Rust, which it is fine as long as the criticism is well
| argued.
| nindalf wrote:
| I'm not putting the bar high. I'm giving an example of
| people who love Rust criticising Rust. Person I replied to
| claimed that Rust fanboys didn't do this because they were
| zealots. That's not true, clearly.
|
| I've read a lot of criticism of Rust and most of it is from
| people who tried it for a weekend, couldn't understand the
| borrow checker and wrote some low quality criticism of it.
| If someone points out problems in that post, they are
| accused of zealotry and fanboyism.
|
| Read the post I linked. It covers all the issues and makes
| the strongest possible case against the language. Then tell
| me if you've ever seen one that is as negative, accurate
| and succinct as that one.
| steveklabnik wrote:
| You've got the contrapositive there. The claim was that
| folks who love Rust do not accept criticism of the
| language. Therefore, a criticism by someone who loves the
| language was presented, to show that claim was false. Your
| parent isn't saying that only folks who love Rust can
| criticize Rust.
| planetis wrote:
| > You shouldn't think that Rust users are fanboys just
| because you see push back to low effort, low knowledge
| critiques.
|
| That's too much assuming, btw I read in this thread a comment
| from a well-known Nim dev working in multithreading (with
| much knowledge on the subject) and it was downvoted to
| oblivion.
| nindalf wrote:
| Could you share a link? I'd be surprised if well founded
| criticism was downvoted.
| planetis wrote:
| https://news.ycombinator.com/item?id=26446082
| gattr wrote:
| To practise Rust, I rewrote my small C99 library in it [1].
| Performance is more or less the same, I only had to use unchecked
| array access in one small hot loop (details in README.md). I
| haven't ported multithreading yet, but I expect Rust's Rayon
| parallel iterators will likewise be comparable to OpenMP.
|
| [1] https://github.com/GreatAttractor/libskry_r
| up2isomorphism wrote:
| Your C library does not check malloc returns and also malloc
| and free everywhere inside library functions are not the best
| way to write a C library.
| gattr wrote:
| As for malloc/free, I'm guessing the recommendation is to
| allow the user to pass their own allocator on library
| initialization?
|
| Non-checked malloc returns - ouch, I count 12 (out of 56)
| without a check. Thanks for pointing this out.
| eqvinox wrote:
| Is it possible to do RCU in Rust? Without unsafe blocks?
| nyanpasu64 wrote:
| @steveklabnik, RCU is different from RwLock in that the single
| writer and all readers never block each other.
|
| Given that RCU is a complex wait-free data structure (though I
| don't fully understand it), I suspect it may not necessarily be
| possible to implement it without unsafe blocks, purely in terms
| of the standard library concurrency types (atomics and Arc can
| be used without unsafe, but themselves contain unsafe blocks).
| The general goal is to create an abstraction which encapsulates
| unsafe blocks such that it's impossible for outside users
| calling safe functions to violate memory safety. Of course,
| libraries sometimes have bugs that need to be fixed.
| steveklabnik wrote:
| Ah yeah, makes sense. I would also imagine it needs unsafe,
| yeah.
| steveklabnik wrote:
| I don't know all of the subtleties, but it sounds like
| https://doc.rust-lang.org/stable/std/sync/struct.RwLock.html to
| me? At least in some way?
| up2isomorphism wrote:
| My experience is that languages survives not because of a
| particular feature, but because they are USEFUL in practice to
| produce a software.
|
| The fact that C is used in so many places speaks for itself about
| it usefulness. And this is done by writing software by majority
| of C programmers instead of jumping on every forum to attack
| other languages, writing extended blog posts just to convince
| people that they "should" switch to the language they like.
|
| Also if you believe bounds check is the most difficult thing in
| software development, it just mean that you haven't dealt with a
| sufficient system yet or you just pretends to be.
|
| The similar thing also applied to that if you think naively
| putting pthread_mutex_lock and unlock around the data structure
| is hard, it just means you haven't touched the scenarios that C
| programmers resorts to non-trivial locking mechanisms for.
| SimpleGoto1080 wrote:
| With the lack of context in this article I'm willing to bet rust
| isn't actually faster than C
| Shadonototro wrote:
| Very biased comparison without actual source or numbers to back
| things
|
| Even more surprising it got to front page
|
| Do people really have low standard of quality on hacker news too?
| 0xdeadfeed wrote:
| It's not really surprising TBH. Everyone knows HN is a pro-Rust
| echo chamber.
| skohan wrote:
| > Both are "portable assemblers"
|
| I don't tend to think of Rust as "portable assembly", and this is
| indeed one of the points where I think it differs the most from
| C. I think of "portable assembly" as being applicable to C,
| because it is some version of a "minimal" level of abstraction
| for a high-level language. Rust is very much a tool for
| abstraction, and one of the USPs of rust is that the compiler
| abstracts away the low-level details of memory management in a
| way which is not as costly as other automatic memory management
| strategies.
|
| Maybe it's due to lack of experience, but with C code it's fairly
| easy to look at a block of code and imagine approximately which
| assembly would be generated. With highly abstract Rust code, like
| with template-heavy C++ code, I don't feel like that at all.
| pornel wrote:
| With a bit of experience you get the same in Rust.
|
| Rust does not abstract away memory management. For example, it
| never heap allocates anything implicitly. It inserts
| destructors, but does so predictably at end of scopes, in a
| specified order.
|
| Rust heavily uses iterators with closures, but these get
| aggressively inlined, and you can rely on them optimizing down
| to a basic loop. For code generation they're not too different
| from a fancy C macro.
|
| And if in doubt, there's https://rust.godbolt.org/ (don't
| forget to add -O to flags)
| benreesman wrote:
| A comparison between Rust and modern C++ would be more
| interesting in my opinion. It seems that those languages are
| closer in the design goal space than either is to C.
| nyc_pizzadev wrote:
| Agreed, came here to say the same thing. Would be interesting
| to see how they stack up against each other. Both are highly
| evolved modern languages that make pretty much the same claims.
| jblow wrote:
| This entire article is nonsense. To a first approximation, the
| speed of your program in 2021 is determined by locality of memory
| access and overhead with regard to allocation and deallocation. C
| allows you to do bulk memory operations, Rust does not (unless
| you turn off the things about Rust that everyone says are good).
| Thus C is tremendously faster.
|
| There is this habit in both academia and industry where people
| say "as fast as C" and justify this by comparing to a
| tremendously slow C program, but don't even know they are doing
| it. It's the blind leading the blind.
|
| The question you should be asking yourself is, "If all these
| claims I keep seeing about X being as fast as Y are true, then
| why does software keep getting slower over time?"
|
| (If you don't get what I am saying here, it might help to know
| that performance programmers consider malloc to be tremendously
| slow and don't use it except at startup or in cases when it is
| amortized by a factor of 1000 or more).
| pornel wrote:
| I don't disagree that memory access is nowadays critical for
| speed, but I haven't found Rust standing in the way of
| optimizing it.
|
| As I've pointed out in the article, Rust does give you precise
| control over memory layout. Heap allocations are explicit and
| optional. In safe code. You don't even need to avoid any nice
| features (e.g. closures and iterators can be entirely on stack,
| no allocations needed).
|
| Move semantics enables `memcpy`ing objects anywhere, so they
| don't have a permanent address, and don't need to be allocated
| individually.
|
| In this regard Rust is different from e.g. Swift and Go, which
| claim to have C-like speed, but will autobox objects for you.
| jblow wrote:
| Bulk operations are not really about layout, they are about
| whether you mentally consider each little data structure to
| be an individual entity with its own lifetime, or not,
| because this determines what the code looks like, which
| determines how fast it is. (Though layout does help with
| regard to cache hits and so forth).
| pornel wrote:
| "mentally"?
|
| I don't know what you're trying to imply that Rust does,
| but I'll reiterate that Rust lifetimes don't exist at code
| generation time. They're not a runtime construct, they have
| _zero_ influence over what code does at run time (e.g.
| mrustc compiler doesn 't implement lifetimes, but
| bootstraps the whole Rust compiler just fine).
|
| If you create `Vec<Object>` in Rust, then all objects will
| be allocated and laid out together as _one_ contiguous
| chunk of memory, same as `malloc(sizeof(struct object) *
| n)` in C. You can also use `[Object; N]` or ArrayVec that
| 's is identical to `struct object arr[N]`. It's also
| possible to use memory pools/arenas.
|
| And where possible, LLVM will autovectorize operations on
| these too. Even if you use an iterator that in source code
| looks like it's operating on individual elements.
|
| Knowing your other work I guess you mean SoA vs AoS? Rust
| doesn't have built-in syntax for these, but neither does C
| that we're talking about here.
| jblow wrote:
| > They're not a runtime construct, they have zero
| influence over what code does at run time (e.g. mrustc
| compiler doesn't implement lifetimes, but bootstraps the
| whole Rust compiler just fine).
|
| This kind of reasoning seems like it makes sense, but
| actually it is false. ("Modern C++" people make the same
| arguments when arguing that you should use "zero-cost
| abstractions" all over the place). Abstractions determine
| how people write code, and the way they write the code
| determines the performance of the code.
|
| When you conceptualize a bunch of stuff as different
| objects with different lifetimes, you are going to write
| code treating stuff as different objects with different
| lifetimes. That is slow.
|
| > If you create `Vec<Object>` in Rust, then all objects
| will be allocated and laid out together as one contiguous
| chunk of memory
|
| Sure, and that covers a small percentage of the use cases
| I am talking about, but not most of them.
| Rusky wrote:
| > When you conceptualize a bunch of stuff as different
| objects with different lifetimes, you are going to write
| code treating stuff as different objects with different
| lifetimes. That is slow.
|
| This is not how lifetimes work at all. In fact this
| sounds like the sort of thing someone who has never read
| or written anything using lifetimes would say: even the
| most basic applications of lifetimes go beyond this.
|
| Fundamentally, any particular lifetime variable (the 'a
| syntax) _erases_ the distinctions between individual
| objects. Rust doesn 't even _have syntax_ for the
| lifetime of any individual object. Research in this area
| tends to use the term "region" rather than "lifetime"
| for this reason.
|
| Lifetimes actually fit in quite nicely with the sorts of
| things programs do to optimize memory locality and
| allocations.
|
| > Sure, and that covers a small percentage of the use
| cases I am talking about, but not most of them.
|
| Fortunately the other stuff you are talking about works
| just fine in Rust as well.
| jblow wrote:
| I am talking about RAII. RAII leads to programs that are
| inherently slow.
| pornel wrote:
| Rust's flavor of RAII is different from C++'s, because
| Rust doesn't have constructors, operator new, implicit
| copy constructors, and doesn't expose moved-out-of state.
|
| Rust also has "Copy" types which by definition can be
| trivially created and can't have destructors. Collections
| take advantage of that (e.g. dropping an array doesn't
| run any code).
|
| So I don't really get what you mean. Rust's RAII can be
| compiled to plain C code (in fact, mrustc does exactly
| that). It's just `struct Foo foo = {}` followed by
| optional user-defined `bye_bye(&foo)` after its last use
| (note: it's not free/delete, memory allocator doesn't
| have to be involved at all).
|
| I suspect you're talking about some wider programming
| patterns and best practices, but I don't see how that
| relates to C. If you don't need per-object
| init()/deinit(), then for the same you wouldn't use RAII
| in Rust either. RAII is an opt-in pattern.
| Rusky wrote:
| RAII is completely orthogonal to lifetimes, for one
| thing. You can have either without the other.
|
| But, I am familiar with the kind of thing you're
| complaining about here, and frankly the mere existence of
| RAII is not its cause. Working with a large dataset,
| managing allocation/layout/traversal in a holistic way,
| you just... don't write destructors for every tiny piece.
| It works fine, I do it all the time (in both Rust and
| C++).
| pornel wrote:
| You haven't really explained in any detail what is slow
| about "treating stuff as objects with different
| lifetimes", and specifically how Rust differs there from
| C. Can you give an example?
|
| Maybe you'd be interested to hear that Rust's borrow
| checker is very friendly to the ECS pattern, and works
| with ECS much better than with the classic OOP "Player
| extends Entity" approach.
| zozbot234 wrote:
| > (If you don't get what I am saying here, it might help to
| know that performance programmers consider malloc to be
| tremendously slow and don't use it except at startup or in
| cases when it is amortized by a factor of 1000 or more).
|
| Rust is now getting support for custom local allocators ala
| C++, including in default core types like Box<>, Vec<> and
| HashMap<>. It's an unstable feature, hence not yet part of
| stable Rust but it's absolutely being worked on.
| jblow wrote:
| Sure, but you are still going to be constrained greatly in
| terms of what those allocators are able to do, are you not?
| steveklabnik wrote:
| In what way? What kind of constraints are you imagining
| here?
| jblow wrote:
| I guess I am confused by the question. The job of the
| borrow checker is to constrain what you are allowed to
| do, and it's well-understood that it constrains you to a
| subset of correct programs, so that you stay in a realm
| that is analyzable.
| steveklabnik wrote:
| Sure, but the borrow checker only operates on references.
| Rust gives you the tools to work with raw everything, if
| you dip into unsafe. Memory allocators, doing this kind
| of low-level thing, don't work with references. Let's say
| you want to implement a global allocator (this is the
| only current API in stable Rust, non-global allocators
| are on the way). The trait you use, which gives you the
| equivalent of malloc/free, has this signature:
| unsafe impl GlobalAlloc { pub unsafe fn
| alloc(&self, layout: Layout) -> *mut u8; pub
| unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
| }
|
| Note the *mut u8 rather than say, &mut u8. Most people
| would not be using this interface directly, they'd be
| using a data structure that uses it internally.
|
| Now, there's a good argument to be had about safe and
| unsafe, how much you need, in what proportion, and in
| what kinds of programs... but when you say things like "C
| allows you to do bulk memory operations, Rust does not"
| and ask about the borrow checker when talking about
| allocators, to someone who is familiar with Rust's
| details, it seems like you are misinformed somehow, which
| makes it really hard to engage constructively with what
| you're saying.
| jblow wrote:
| I'll try to further bridge some of the understanding gap.
|
| People in this thread keep talking about "arena
| allocators" as if they are special things that you would
| use a few times (Grep Guy said this above, for example),
| or, here you imply they would be used internally to data
| structures, in a way that doesn't reach out to user-
| level.
|
| That makes them not nearly as useful as they can be!
|
| The game we are working on is currently 100k lines (150k
| lines if you count comments etc), and almost all
| allocations in the entire program are of this bulk type,
| in one way or another. Like if I want to copy a string to
| modify it a little bit to then look up a bitmap or make a
| filename, those are temporary allocations at user level
| and are not wrapped by anything. The string type is not
| some weird heavyweight C++ std::string kind of thing that
| wraps a bunch of functionality, it is just a length and a
| data pointer.
|
| So the proposal to use unsafe in this kind of context
| doesn't make sense, since then you are putting unsafe
| everywhere in the program, which, then, why pretend you
| are checking things?
|
| You can say, "well you as the end-user shouldn't be doing
| this stuff, everything should be wrapped in structures
| that were written by someone smarter than you I guess,"
| but that is just not the model of programming that I am
| doing.
|
| I understand how you can think the statement "Rust does
| not (allow you to do bulk memory operations)" is false,
| but when I say this, part of what I am including in "bulk
| memory operations" is the ability (as the end user) to
| pretend like you are in a garbage-collected language and
| not worry about the lifetime of your data, without having
| to take the performance penalty of using a garbage-
| collected language. So if you add back in worrying about
| lifetimes, it's not the same thing.
|
| If you think "bulk memory allocation" is like, I have
| this big data structure that manages some API and it has
| some linked lists, and instead of allocating those nodes
| on the heap I get them from an arena or pool managed by
| the bigger structure ... that's fine, it is better than
| not doing it, but it doesn't help the end user write
| simpler code, and in practical terms it means that most
| of the allocations in the program are going to be non-
| bulk, because there's just too much friction on doing
| them broadly.
|
| If it helps, I can revise my statement to "Rust enables
| you to do certain kinds of internal bulk memory
| allocation, but using bulk allocation broadly and freely
| across your program goes against the core spirit of the
| language" ... that sounds pretty uncontroversial? Then to
| bring it back to the original post, I would say, "This
| kind of broad use of bulk allocation is important for
| high performance and simplicity of the resulting code."
|
| One last note, I am pretty tired of the "you don't
| understand Rust, therefore you are beneath us" line that
| everyone in the Rust community seems to deploy with even
| the slightest provocation -- not just when responding to
| me, but to anyone who doesn't just love Rust from top to
| bottom. Really it makes me feel that the only useful
| thing to do is just ignore Rust folks and go do useful
| things instead. I know I am not the only person who feels
| this way.
| Ar-Curunir wrote:
| Rustc widely uses arenas, and so do many other rust
| programs to maintain object pools. It's not an arcane
| process in Rust.
| Rusky wrote:
| This style of memory management can be as pervasive as
| you like! You are reading way more detail out of people's
| comments than they put there, and then getting upset
| about your misinterpretation.
|
| If every throwaway string in your program comes from an
| arena that you clear later, great! Rust won't stop you,
| or even force you to use unsafe every time you build one.
| The unsafe code goes in a "give me a fresh chunk of
| temporary memory" function, and that function is safe to
| call all over the place: unsafe-in-a-safe-function is a
| common pattern for _extending_ the set of analyzable
| programs.
|
| (It's also worth pointing out that Rust's primitive
| string type _is_ "just a length and a data pointer," so
| once you've allocated one out of an arena like this, you
| can do all the nice built-in string-y things with it,
| with no std::string-like interference.)
|
| The Rust compiler itself uses this sort of bulk memory
| all the time. It's not limited to the internals of data
| structures there- it's spread across larger phases and
| queries of its operation, with all kinds of stuff
| allocated the same way.
|
| Now, to be fair, this is not the default- e.g. Rust's
| standard library of collections don't participate. But
| this is why everyone keeps mentioning custom allocators
| to you- there is ongoing work to extend these collections
| with the ability to control how they perform their
| allocation!
|
| > One last note, I am pretty tired of the "you don't
| understand Rust, therefore you are beneath us" line that
| everyone in the Rust community seems to deploy with even
| the slightest provocation -- not just when responding to
| me, but to anyone who doesn't just love Rust from top to
| bottom.
|
| You would get this kind of reaction a lot less often if
| you didn't make vague or nonsense claims about it so
| often.
| jblow wrote:
| Okay, but if I do this everywhere, then I de facto don't
| have memory safety. Why, then should I use Rust and
| pretend like I am getting memory safety? Why wouldn't I
| use a lower-friction language with a faster compiler? It
| looks to me like the Rust community has this weird way of
| wanting to have its cake, and eat it too, about memory.
| Y'all want to advertise how important memory safety is,
| how great it is to have, and so forth. Then in cases like
| this, it's always "oh but you just use unsafe, it's
| fine". These stories are mutually inconsistent. Either
| you have memory safety or you don't. Paying the cost that
| Rust makes programmers pay for memory safety, and then
| not actually getting memory safety, is the worst of both
| worlds.
|
| Then when you guys say I am making nonsense claims
| because of course you can have your cake and also eat it
| as long as you use the Rust programming language, well,
| it's just pretty weird at that point.
| steveklabnik wrote:
| > here you imply they would be used internally to data
| structures, in a way that doesn't reach out to user-
| level.
|
| Ah! I think I am understanding you a bit better. The
| thing is, ultimately, Rust is as flexible as you want it
| to be, and so there are a variety of options. This can
| make it tricky, when folks are talking about slightly
| different things, in slightly different contexts.
|
| When you say "doesn't reach out to user level," what I
| mean by what I said was that users don't generally call
| alloc and dealloc directly. Here, let's move to an actual
| concrete example so that it's more clear. Code is better
| than words, often: use bumpalo::{Bump,
| boxed::Box}; struct Point { x:
| i32, y: i32, } fn main()
| { let bump = Bump::with_capacity(256);
| let c = Box::new_in(Point { x: 5, y: 6 }, &bump);
| }
|
| This is using "bumpalo", a very straightforward bump
| allocator. As a user, I say "hey, I want an arena backed
| by 256 bytes. Please allocate this Point into it, and
| give me a pointer to it." "c" here is now a pointer into
| this little heap it's managing. Because my points are
| eight bytes in size, I could fit 32 points here. Nothing
| will be deallocated until bump goes out of scope.
|
| But notably, I am not using any unsafe here. Yes, I am
| saying "give me an allocation of this total size", and
| yes I am saying "please allocate stuff into it and give
| me pointers to it," but generally, I as a user don't need
| to mess with unsafe unless I'm the person implementing
| bumpalo. And sometimes you are! Personally, I work in
| embedded, with no global heap at all. I end up using more
| unsafe than most. But there's no unsafe code in what I've
| written above, but it's still gonna give you something
| like what you said you're doing in your current game. Of
| course, you probably want something more like an arena,
| than a pure bump allocator. Those exist too. You write
| 'em up like you would anything else. Rust will still make
| sure that c doesn't outlive bump, but it'll do that
| entirely at compile time, no runtime checks here.
|
| Oh, and this is sorta random but I didn't know where to
| put it: Rust's &str type is a "pointer + length" as well.
| Using this kind of thing is _extremely_ common in Rust,
| we call them "slices" and they're not just for strings.
|
| > You can say, "well you as the end-user shouldn't be
| doing this stuff, everything should be wrapped in
| structures that were written by someone smarter than you
| I guess," but that is just not the model of programming
| that I am doing.
|
| While that's convenient, and in this case, I am showing
| that, the point is that it's about encapsulation. I don't
| _have_ to use this existing allocator if I wanted to
| write something different. But because I can encapsulate
| the unsafe bit, no matter who is writing it, I need to
| pay attention in a smaller part of my program. Maybe I am
| that person, maybe someone else is, but the benefit is
| roughly the same either way.
|
| > So if you add back in worrying about lifetimes, it's
| not the same thing.
|
| To be super clear about it, Rust has raw pointers, that
| are the same as C. No lifetimes. If you want to use them,
| you can. The vast, vast, vast majority of the time, you
| do not need the flexibility, and so it's worth giving it
| up for the compile time checks.
|
| > If you think "bulk memory allocation" is like...
|
| It's not clear to me above if the API I'm talking about
| above is what you mean here, or something else. It's not
| clear to me how you'd get simpler than "please give me a
| handle to this part of the heap," but I haven't seen your
| latest Jai streams. I am excited to give it a try once I
| am able to.
|
| > but using bulk allocation broadly and freely across
| your program goes against the core spirit of the language
|
| I don't know why you'd think these techniques are against
| the core spirit of the language. Rust's primitive array
| type is literally "give me N of these bits of data laid
| out next to each other in memory." We had a keynote at
| Rustconf about how useful generational arenas are as a
| technique in Rust. As a systems language, Rust needs to
| give you the freedom to do literally anything and
| everything possible.
|
| > One last note, I am pretty tired of the "you don't
| understand Rust, therefore you are beneath us"
|
| To be clear, I don't think that you or anyone else is
| "beneath us," here. What I want is informed criticism,
| rather than sweeping, incorrect statements that lead
| people to believe things that aren't true. Rust is not
| perfect. There are tons of things we could do better. But
| that doesn't mean that it's not right to point out when
| facts are different than the things that are said. You of
| all people seem to appreciate a forward communication
| style.
| burntsushi wrote:
| > People in this thread keep talking about "arena
| allocators" as if they are special things that you would
| use a few times (Grep Guy said this above, for example)
|
| I didn't say anything about arena allocators. What I said
| was that amortizing allocation was routine and
| commonplace in ripgrep's code. I definitely wouldn't say
| that amortizing allocation is "special" or something I
| use a "few" times. As one example of amortizing allocs,
| it's very common to ask the caller for some memory
| instead of allocating memory yourself.
|
| > I am pretty tired of the "you don't understand Rust,
| therefore you are beneath us" line that everyone in the
| Rust community seems to deploy with even the slightest
| provocation
|
| Kind of like opening a comment with "This entire article
| is nonsense." Right? Snubbing your nose and then getting
| miffed by the perception of others snubbing their nose at
| you is a bunch of shenanigans. And then you snub your
| nose at pretty much everyone: "It's the blind leading the
| blind." I mean, c'mon dude.
|
| The problem with your comments is that they lack
| specifics. Even after this comment where you've tried to
| explain, it's pretty hard for me to understand what
| you're getting at. I suspect part of the problem is your
| use of this term "bulk allocation." Is it jargon that
| refers to the specific pattern you have in mind? Because
| if it is, I can't find it documented anywhere after a
| quick search. If it's not jargon, then "bulk allocation"
| could mean _a lot_ of things, but you clearly have a very
| specific variant of it in mind.
|
| It's clear to me that your argument is a very subtle one
| that requires nuance and probably lots of code examples
| to get the point across. Going about this at the other
| end---with lots of generalities and presumptions---just
| seems like a fool's errand.
| [deleted]
| Kutta wrote:
| Arenas have been used in Rust for a long time.
| burntsushi wrote:
| > To a first approximation, the speed of your program in 2021
| is determined by locality of memory access and overhead with
| regard to allocation and deallocation.
|
| I wouldn't call that a first approximation. Take ripgrep as an
| example. In a checkout of the Linux kernel with everything in
| my page cache: $ time rg zqzqzqzq -j1
| real 0.609 user 0.315 sys 0.286
| maxmem 7 MB faults 0 $ time rg zqzqzqzq
| -j8 real 0.116 user 0.381
| sys 0.464 maxmem 9 MB faults 0
|
| This alone, to me, says "to a first approximation, the speed of
| your program in 2021 is determined by the number of cores it
| uses" would be better than your statement. But I wouldn't even
| say that. Because performance is complicated and it's difficult
| to generalize.
|
| Using Rust made it a lot easier to parallelize ripgrep.
|
| > C allows you to do bulk memory operations, Rust does not
| (unless you turn off the things about Rust that everyone says
| are good). Thus C is tremendously faster.
|
| Talk about nonsense. I do bulk memory operations in Rust all
| the time. Amortizing allocation is exceptionally common in
| Rust. And it doesn't turn off anything. It's used in ripgrep in
| several places.
|
| > There is this habit in both academia and industry where
| people say "as fast as C" and justify this by comparing to a
| tremendously slow C program, but don't even know they are doing
| it. It's the blind leading the blind.
|
| I've never heard anyone refer to GNU grep as a "tremendously
| slow C program."
|
| > The question you should be asking yourself is, "If all these
| claims I keep seeing about X being as fast as Y are true, then
| why does software keep getting slower over time?"
|
| There are many possible answers to this. The question itself is
| so general that I don't know how to glean much, if anything,
| useful from it.
| jblow wrote:
| > This alone, to me, says "to a first approximation, the
| speed of your program in 2021 is determined by the number of
| cores it uses" would be better than your statement. But I
| wouldn't even say that.
|
| You chose an embarrassingly parallel problem, which most
| programs are not. So you cannot generalize this example
| across most software. When you try to parallelize a
| structurally complicated algorithm, the biggest issue is
| contention. I was leaving this out because it really is a 2nd
| order problem -- most software today would get faster if you
| just cleaned up its memory usage, than if you just tried to
| parallelize it. (Of course it'd get even faster if you did
| both, but memory is the E1).
|
| > There are many possible answers to this.
|
| How come so few people are concerned with the answers to that
| question and which are true, but so many people are concerned
| with making performance claims?
| howinteresting wrote:
| Thanks for making The Witness, Jonathan. It's one of my
| favorite games of all time and an exemplar of what it means
| to work through the consequences of logical axioms.
|
| Makes me all the more sad that you're consistently unable
| to work through the consequences of Rust's axioms.
| burntsushi wrote:
| > You chose an embarrassingly parallel problem
|
| Well, I mean, you chose an embarrassingly general statement
| to make? Play stupid games, win stupid prizes.
|
| > which most programs are not
|
| Programs? Or problems? Who says? It's not at all obvious to
| me that it's true. And even if it were true,
| "embarrassingly parallel" problems are nowhere close to
| uncommon.
|
| > When you try to parallelize a structurally complicated
| algorithm, the biggest issue is contention.
|
| With respect to performance, I agree.
|
| > How come so few people are concerned with the answers to
| that question and which are true, but so many people are
| concerned with making performance claims?
|
| The question is itself flawed. Technology isn't fixed. We
| "advance" and try to do more stuff. This is not me saying,
| "this explains everything." Or even that "more stuff" is a
| good thing. This is me saying, "there's more to it than
| your over-simplifications."
| jblow wrote:
| If you do not understand that "embarrassingly parallel"
| is a technical term and that it's generally understood
| that most programs are not easily parallelizable, there
| is not a discussion we can have here.
| Rusky wrote:
| Really feels like you're just digging yourself deeper
| into a hole here:
|
| Burntsushi began with "here, parallelization is beating
| out memory locality and optimization in its impact," but
| _explicitly declined to generalize this_ the way you
| generalized your claim about memory.
|
| He further pointed out that ripgrep is fast not just
| because of parallelization, but also because of how it
| handles memory.
|
| Then you come back with "you can't always parallelize
| this well" (which burntsushi agreed with from the
| beginning) and "you also need to deal with memory" (which
| ripgrep does)? How is this burntsushi's problem with
| understanding "embarrassingly parallel" and not your
| problem with understanding Rust?
| burntsushi wrote:
| I agree that a discussion is difficult. Your comments are
| so vague and generalized that it's not clear what you're
| talking about at all. Bring something more specific to
| the table like the OP did instead of pontificating on
| generalities.
| johndoe42377 wrote:
| First non-bullshit article about Rust for long time.
| discardable_dan wrote:
| A graph would be good. Any graph. Preferably multiple. Otherwise,
| this is all empirical data. Show me why Rust wins, and how.
| Telling me "doubly-linked lists are slow" is not useful, as a
| developer considering one of these two languages.
| mhh__ wrote:
| Graphs are empirical data, surely.
|
| All benchmarks should be delivered in the form of a graph and
| histogram, I had to close a PR recently where the
| "optimization" was 1% of a standard deviation away from the
| mean _without_ even running either implementation!
| brundolf wrote:
| This isn't that type of post. Sometimes what's useful is a
| brain-dump of heuristics and tidbits and general impressions
| formed over years and years of experience. Sometimes that's
| more useful, or even more accurate, than hard benchmark data.
| 0xdeadfeed wrote:
| Show me some numbers please, or I'll just take it as another list
| of wishes that Rust fans think/want to be true.
| bambam24 wrote:
| Rust is low-level enough that if necessary, it can be optimized
| for maximum performance just as well as C. Higher-level
| abstractions, easy memory management, and abundance of available
| libraries tend to make Rust programs have more code, do more, and
| if left unchecked, can add up to bloat. However, Rust programs
| also optimize quite well, sometimes better than C. While C is
| good for writing minimal code on byte-by-byte pointer-by-pointer
| level, Rust has powerful features for efficiently combining
| multiple functions or even whole libraries together.
|
| But the biggest potential is in ability to fearlessly parallelize
| majority of Rust code, even when the equivalent C code would be
| too risky to parallelize. In this aspect Rust is a much more
| mature language than C.
| Aissen wrote:
| > Rust can't count on OSes having Rust's standard library built-
| in, so Rust executables bundle bits of the Rust's standard
| library (300KB or more). Fortunately, it's a one-time overhead.
|
| No, it's not, especially if you have multiple binaries. There are
| hacks, like using a multi-call single binary, (forget about file-
| based privilege separation), or using an unmaintained fork of
| cargo to build a rust toolchain capable of dynamic linking
| libstd. See: https://users.rust-lang.org/t/link-the-rust-
| standard-library... and https://github.com/johnthagen/min-sized-
| rust
|
| I'd be interested in any up-to-date trick to do better than this.
| moonchild wrote:
| > C libraries typically return opaque pointers to their data
| structures, to hide implementation details and ensure there's
| only one copy of each instance of the struct. This costs heap
| allocations and pointer indirections. Rust's built-in privacy,
| unique ownership rules, and coding conventions let libraries
| expose their objects by value
|
| The primary reason c libraries do this is not for safety, but to
| maintain ABI compatibility. Rust eschews dynamic linking, which
| is why it doesn't bother. Common lisp, for instance, does the
| same thing as c, for similar reasons: the layout of structures
| may change, and existing code in the image has to be able to deal
| with it.
|
| > Rust by default can inline functions from the standard library,
| dependencies, and other compilation units. In C I'm sometimes
| reluctant to split files or use libraries, because it affects
| inlining
|
| This is again because c is conventionally dynamically linked, and
| rust statically linked. If you use LTO, cross-module inlining
| will happen.
| dan-robertson wrote:
| The reason Common Lisp uses pointers is because it is
| dynamically typed. It's not some principled position about ABI
| compatibility. If I define an RGB struct for colours, it isn't
| going to change but it would still need to be passed by
| reference because the language can't enforce that the variable
| which holds the RGBs will only ever hold 3 word values.
| Similarly, the reason floats are often passed by reference
| isn't some principled stance about the float representation
| maybe changing, it's that you can't fit a float and the
| information that you have a float into a single word[1].
|
| If instead you're referring to the fact that all the fields of
| a struct aren't explicitly obvious when you have such a value,
| well I don't really agree that it's always what you want. A
| great thing about pattern matching with exhaustiveness checks
| is that it forces you to acknowledge that you don't care about
| new record fields (though the Common Lisp way of dealing with
| this probably involves CLOS instead).
|
| [1] some implementations may use NaN-boxing to get around this
| kazinator wrote:
| Lisp users pointers because of the realization that the
| entities in a computerized implementation of symbolic
| processing can be adequately represented by tiny index tokens
| that fit into machine registers, whose properties are
| implemented elsewhere, and these tokens can be whipped around
| inside the program very quickly.
| dan-robertson wrote:
| What your describing are symbols where the properties are
| much less important than the identity. Most CL
| implementations will use fixnums rather than pointers when
| possible because they don't have some kind of philosophical
| affinity to pointers. For data structures, pointers aren't
| so good with modern hardware. The reason Common Lisp tends
| to have to use pointers is that the type system cannot
| provide information about how big objects are. Compare this
| to the arrays which are often better at packing because
| they can know how big their elements are.
|
| This is similar in typed languages with polymorphism like
| Haskell or ocaml where a function like concat (taking a
| list of lists to a single list) needs to work when the
| elements are floats (morally 8 bytes each) or bools
| (morally 1 bit each). The solution is to write the code
| once and have everything be in one word, either a fixnum or
| a pointer.
| pharmakom wrote:
| Rust makes building from source and cross compiling so easy
| that I don't really care for dynamic linking in my use cases of
| Rust.
| skohan wrote:
| Dynamic linking is one thing I miss from Swift - I used
| dynamic linking for hot code reloading for several
| applications, which resulted in super fast and useful
| development loops. Given Rust's sometimes long compile times,
| this is something which would be welcome.
| jdright wrote:
| There are crates for hot reloading in Rust, and they use
| dynamic linking.
| skohan wrote:
| Do you have to stick to a C-FFI like interface, or can
| they handle rust-native features like closures and
| traits?
| howinteresting wrote:
| Some stick to C FFI, some enforce that the Rust compiler
| version is the same which makes ABI issues irrelevant.
| rectang wrote:
| > _ABI compatibility_
|
| Rust provides ABI compatibility against its C ABI, and if you
| want you can dynamically link against that. What Rust eschews
| is the insane fragile ABI compatibility of C++, which is a huge
| pain to deal with as a user:
|
| https://community.kde.org/Policies/Binary_Compatibility_Issu...
|
| I don't think we'll ever see as comprehensive an ABI out of
| Rust as we get out of C++, because exposing that much
| incidental complexity is a bad idea. Maybe we'll get some
| incremental improvements over time. Or maybe C ABIs are the
| sweet spot.
| anfilt wrote:
| Rust has yet to standardize an ABI. Yes you can call or
| expose a function with C calling conventions. However, you
| cant pass all native rust types like this, and lose some
| semantics.
|
| However, as the parent comment you responded to you can
| enable LTO when compiling C. As rust is mostly always
| statically linked it basically always got LTO optimizations.
| johncolanduoni wrote:
| Even with static linking, Rust produces separate
| compilation units a least at the crate level (and depending
| on compiler settings, within crates). You won't get LTO
| between crates if you don't explicitly request it. It does
| allow inlining across compilation units without LTO, but
| only for functions explicitly marked as `#[inline]`.
| moonchild wrote:
| Swift has a stable ABI. It makes different tradeoffs than
| rust, but I don't think complexity is the cliff. There is a
| good overview at https://gankra.github.io/blah/swift-abi/
| kelnos wrote:
| Swift has a stable ABI at the cost of what amounts to
| runtime reflection, which is expensive. That doesn't really
| fit with the goals of Rust, I don't think.
| saagarjha wrote:
| This is misleading, especially since Swift binaries do
| typically ship with actual reflection metadata (unless it
| is stripped out). The Swift ABI _does_ keep layout
| information behind a pointer in certain cases, but if you
| squint at it funny it 's basically a vtable but for data.
| (Actually, even more so than non-fragile ivars are in
| Objective-C, because I believe actual offsets are not
| provided, rather you get getter/setter functions...)
|
| I don't disagree that Rust probably would not go this
| way, but I think that's less "this is spooky reflection"
| and more "Rust likes static linking and cares less about
| stable ABIs, plus the general attitude of 'if you're
| going to make an indirect call the language should make
| you work for it'".
| [deleted]
| skohan wrote:
| Do you have a source on this? I didn't think Swift
| requires runtime reflection to make calling across module
| boundaries work - I thought `.swiftmodule` files are
| essentially IR code to avoid this
| kelnos wrote:
| Pretty sure the link the parent (to my comment) provided
| explains this.
|
| It's not the same kind of runtime reflection people talk
| about when they (for example) use reflection in Java.
| It's hidden from the library-using programmer, but the
| calling needs to "communicate" with the library to figure
| out data layouts and such, and that sounds a lot like
| reflection to me.
| quietbritishjim wrote:
| That's ABI compatibility of the _language_ , not of a
| particular API.
|
| If you have an API that allows the caller to instantiate a
| structure on the stack and pass a reference to it to your
| function, then the caller must now be recompiled when the
| size of that structure changes. If that API now resides in a
| separate dynamic library, then changing the size of the
| structure is an ABI-breaking change, regardless of the
| language.
| moonchild wrote:
| Yes, and if you use the C abi to dynamically link rust code,
| you will have exactly the same problem as c: you can't change
| the layout of your structures without breaking compatibility,
| unless you use indirecting wrappers.
| gspr wrote:
| Rust seems great to me, but aren't we losing a lot by giving
| up on C's dynamic linking and shared libraries?
| hctaw wrote:
| Some would argue you gain more than you lose.
|
| Also to be pedantic, C doesn't spec anything about linkage.
| Shared objects and how linkers use them to compose programs
| is a system detail more than a language one.
| dr-ando wrote:
| I am sympathetic to the point you make but to be accurate,
| one can consume and create C and C compatible dynamic
| libraries with rust. So, one is not "losing" something
| because what you (and me) want - dynamic linking and shared
| libraries with a stable and safe rust ABI - was not there
| to begin with.
| kazinator wrote:
| > _This costs heap allocations and pointer indirections._
|
| Heap allocations, yes; pointer indirections no.
|
| A structure is referenced by pointer no matter what. Remember
| that the stack is accessed via a _stack pointer_.
|
| The performance cost is that there are no inline functions for
| a truly opaque type; everything goes through a function call.
| Indirect access through functions is the cost, which is worse
| than a mere pointer indirection.
|
| An API has to be well-designed this regard; it has to
| anticipate the likely use cases that are going to be
| performance critical and avoid perpetrating a design in which
| the application has to make millions of API calls in an inner
| loop. Opaqueness is more abstract and so it puts designers on
| their toes to create good abstractions instead of "oh, the user
| has all the access to everything, so they have all the rope
| they need".
|
| Opaque structures don't have to cost heap allocations either.
| An API can provide a way to ask "what is the size of this
| opaque type" and the client can then provide the memory, e.g.
| by using alloca on the stack. This is still future-proof
| against changes in the size, compared to a compile-time size
| taken from a "sizeof struct" in some header file. Another
| alternative is to have some worst-case size represented as a
| type. An example of this is the POSIX struct sockaddr_storage
| in the sockets API. Though the individual sockaddrs are not
| opaque, the concept of providing a non-opaque worst-case
| storage type for an opaque object would work fine.
|
| There can be half-opaque types: part of the structure can be
| declared (e.g. via some struct type that is documened as "do
| not use in application code"). Inline functions use that for
| direct access to some common fields.
| pornel wrote:
| Escape analysis is tough in C, and data returned by pointer
| may be pessimistically assumed to have escaped, forcing exact
| memory accesses. OTOH on-stack struct is more likely to get
| fields optimized as if they were local variables. Plus x86
| has special treatment for the stack, treating it almost like
| a register file.
|
| Sure, there are libraries which have `init(&struct,
| sizeof(struct))`. This adds extra ABI fragility, and doesn't
| hide fields unless the lib maintains two versions of a
| struct. Some libraries that started with such ABI end up
| adding extra fields behind internal indirection instead of
| breaking the ABI. This is of course all solvable, and there's
| no hard limit for C there. But different concerns nudge users
| towards different solutions. Rust doesn't have a stable ABI,
| so the laziest good way is to return by value and hope the
| constructor gets inlined. In C the solution that is both
| accepted as a decent practice and also the laziest is to
| return malloced opaque struct.
| spacechild1 wrote:
| > This costs heap allocations
|
| I'd like to point out that this is not always the case. Some
| libraries, especially those with embedded systems in mind,
| allow you to provide your own memory buffer (which might live
| on the stack), where the object should be constructed. Others
| allow you to pass your own allocator.
| 0xdeadfeed wrote:
| > While C is good for writing minimal code on byte-by-byte
| pointer-by-pointer level,
|
| Billions of cars with multi-billion ECUs, practically every
| device running an OS, and several NASA rovers disagree.
| brundolf wrote:
| > For example, in C I'd be tempted to reuse a buffer allocated
| for one purpose for another purpose later (a technique known as
| HEARTBLEED)
|
| Pahaha
| dig1 wrote:
| > For example, in C I'd be tempted to reuse a buffer allocated
| for one purpose for another purpose later (a technique known as
| HEARTBLEED).
|
| You can do that in Java (with byte arrays) or in Common Lisp, so
| what is the point here? It is not practice in Java, Lisp nor in C
| and C++.
|
| > It's convenient to have fixed-size buffers for variable-size
| data (e.g. PATH_MAX) to avoid (re)allocation of growing buffers
|
| This is because OS/Kernel/filesystem guarantee path max size.
|
| > Idiomatic Rust still gives a lot control over memory
| allocation, and can do basics like memory pools, ... but in
| general it steers users towards "boring" use or memory.
|
| The same is done by sane C libraries (e.g. glib).
|
| > Every operating system ships some built-in standard C library
| that is ~30MB of code that C executables get for "free", e.g. a
| "Hello World" C executable can't actually print anything, it only
| calls the printf shipped with the OS.
|
| printf is not shipped with the OS, but with libc runtime. It
| doesn't have to be runtime (author needs to learn why this libc
| runtime is shared library and not the usually statically linked
| library) and you can use minimal implementations (musl) if you
| want static binaries with minimal size.
|
| So you are saying Rust doesn't call (g)libc at all and directly
| invoke kernel interrupts? Sure, you can avoid this print
| "overhead" in C with 3-4 lines of inline assembly, but, why?
|
| > Rust by default can inline functions from the standard library,
| dependencies, and other compilation units.
|
| So do C compiler.
|
| > In C I'm sometimes reluctant to split files or use libraries,
| because it affects inlining and requires micromanagement of
| headers and symbol visibility.
|
| Functions doesn't have to be in headers to be inlined.
|
| > C libraries typically return opaque pointers to their data
| structures, to hide implementation details and ensure there's
| only one copy of each instance of the struct. This costs heap
| allocations and pointer indirections. Rust's built-in privacy,
| unique ownership rules, and coding conventions let libraries
| expose their objects by value, so that library users decide
| whether to put them on the heap or on the stack. Objects on the
| stack can can be optimized very aggressively, and even optimized
| out entirely.
|
| WTF? Stopped reading after this.
|
| I find this post a random nonsense and I'd urge author to read
| some serious C book.
| scottlamb wrote:
| And I find your comment to be a super-annoying combination of
| pedantic and mostly wrong. I'm not going to go through every
| example but just pick a few:
|
| > > For example, in C I'd be tempted to reuse a buffer
| allocated for one purpose for another purpose later (a
| technique known as HEARTBLEED).
|
| > You can do that in Java (with byte arrays) or in Common Lisp,
| so what is the point here? It is not practice in Java, Lisp nor
| in C and C++.
|
| C is a really old language with ancient libraries that are
| still widely used even though they are simply bad by modern
| standards. For that reason, I roll my eyes when people say
| something is not practice in C or talk about "sane" C
| libraries. A big part of working with C is dealing with ancient
| insanity.
|
| You can make much stronger statements about what is idiomatic
| in Rust (and to some extent Java) simply because it's newer and
| more cohesive.
|
| > > It's convenient to have fixed-size buffers for variable-
| size data (e.g. PATH_MAX) to avoid (re)allocation of growing
| buffers
|
| > This is because OS/Kernel/filesystem guarantee path max size.
|
| I think you've got that backwards. There's an advertised max
| path size because people wanted to stick paths in fixed-size
| buffers rather than deal with dynamic allocation. PATH_MAX is
| fairly arbitrary considering that there are certainly ways of
| creating and opening files which have paths exceeding that
| limit. I found this doc talking about this:
| https://eklitzke.org/path-max-is-tricky
|
| > printf is not shipped with the OS, but with libc runtime.
|
| "The OS" doesn't mean "the kernel". Read...anything...even the
| lackluster wikipedia article about operating systems...and
| you'll see stuff like GUIs described as part of the OS. They
| (generally) don't mean those are in the kernel. You can also
| see this for example in the GNU GPL; they call out "system
| libraries", which certainly includes libc.
|
| > So you are saying Rust doesn't call (g)libc at all and
| directly invoke kernel interrupts? Sure, you can avoid this
| print "overhead" in C with 3-4 lines of inline assembly, but,
| why?
|
| Rust's own standard library uses libc's system call wrappers
| but not stdio. It has its own libraries for buffer management
| and formatting which provide the safety one would expect of
| Rust, know how to integrate with Rust's Display trait for
| formatting arbitrary Rust data structures, etc. You could call
| libc::printf yourself if you wanted to, but that's not
| idiomatic. I wrote some Rust code calling libc::vsnprintf just
| the other day, but that's because I got a format string +
| va_list from a C log callback, not because I wanted to...
| ben0x539 wrote:
| Any book you'd recommend to back up your claims?
| vll100 wrote:
| How about learning some C instead of asking passive
| aggressive questions all over this discussion?
| ben0x539 wrote:
| You'll be relieved to know I've been writing C code of
| varying quality for like fifteen years on and off. These
| days I get asked to write Go most of the time and we've
| gotten rid of the last C codebase we were maintaining a
| while ago, but I'm always down for discussion of strict
| aliasing rules or stupid preprocessor tricks.
| dig1 wrote:
| "The C Programming Language" from K&R is something everyone
| should read, even if they are not fond of C.
|
| "Expert C Programming" [1]. Not up to date, but written from
| a C compiler writer standpoint. A lot of references to why C
| (and libs) are the way they are.
|
| [1] https://www.amazon.com/Expert-Programming-Peter-van-
| Linden/d...
| carols10cents wrote:
| How does K&R back up the claims you've made here?
| morty_s wrote:
| Modern C, by Jens Gustedt is one of the best books on C that
| I have read. That said, I don't think it scratches the
| surface of backing up the parents claims--though if anyone
| know of such a text please let me know.
| brwell wrote:
| > "Clever" memory use is frowned upon in Rust. In C, anything
| goes.
|
| No, it does not. If Rust programmers don't have discipline in C,
| other people have.
|
| And don't drag out some random CVE numbers again. These are about
| a _fraction_ of existing C projects, many of them were started
| 1980-2000.
|
| It is an entirely different story if a project is started with
| sanitizers, Valgrind and best practices.
|
| I'm not against Rust, except that they managed to take OCaml
| syntax and make it significantly worse. It's just ugly and looks
| like design by committee.
|
| But the evangelism is exhausting. I also wonder why corporations
| are pushing Rust. Is it another method to take over C projects
| that they haven't assimilated yet?
| pjmlp wrote:
| Most surveys place the use of static analysis tools at about
| 11%, and they all go back to early 80's.
|
| Some people are hard learners.
| im3w1l wrote:
| I think it's simply the power of defaults. If it takes an
| extra step then a lot of people wont do it.
| gridspy wrote:
| > The evangelism is exhausting.
|
| My best guess is that people who are "stuck" working in C or
| C++ wish they could use Rust at their Jobs.
|
| Or that others would make the leap and get over the learning
| curve.
| pjmlp wrote:
| Not until it reaches the same level as Visual Studio, Android
| Studio, QtCreator, XCode, CUDA and SYSCL tooling for
| graphical applications and GPGPU.
|
| For anything else managed languages are a much more
| productive option, other than writing kernel and drivers.
| c-cube wrote:
| We get that you don't like rust. But it seems like a lot of
| people currently using C or C++ while like to use rust at
| work, and might disagree about the benefits of the language
| and tooling. I personally know a few friends in distinct
| domains who work on established C++ codebases and are in
| this situation.
|
| There are also a lot of people who do not use C or C++, but
| use a bit of rust because it's so much easier to write fast
| little tools with it. I'm in this category. I even use
| threads sometimes, and it's reasonably easy. A crop of new
| unixy tools in rust seems to indicate other people also
| think alike.
| pjmlp wrote:
| Quite on the contrary, Rust is the ideal language to
| replace C and C++ where automaric memory option is a no
| go, like MISRA-C, kernel and device drivers.
|
| Liking a programing language doesn't make me blind to
| what use cases it actually makes sense to use it, I don't
| see nails everywhere.
| gridspy wrote:
| I agree.
|
| I think you'll find that most rational advocates for any
| language agree that their favorite language is only
| strong in its subdomain.
|
| Any compiled language is more painful than a quick
| scripting one for quick projects where the project
| complexity is low and the language overhead doesn't
| matter.
|
| Rust is substantially more painful to get compiling (due
| to the borrow checker) and harder to debug (due to tool
| maturity) than C# or C++. It's much harder to use than
| Python. Every language has its place.
|
| But when you are investing the time to make an efficient,
| high performance program... or you have limited
| requirements like you said -- Rust becomes a great
| choice.
|
| Every langauge has its place. I'm just dreadfully excited
| that we have a new choice now to trade a bit more time
| interacting with compiler errors for high performance and
| stability -- when that makes sense.
| howinteresting wrote:
| Have you ever had to deal with tail latency due to memory
| pressure on web or backend services?
|
| Command-line tools are also ideal for Rust because startup
| performance matters a lot there.
| pjmlp wrote:
| Yes, writing distributed systems since 1999.
|
| What was done in C, C++ and Tcl, I nowadays use Java and
| .NET languages.
|
| If we really need something low level that either Java or
| .NET cannot offer, a native library for a specific
| component will do, no need to throw the whole thing away
| and do one of those rewrite blog posts.
| howinteresting wrote:
| Tail latency due to memory pressure tends to be inherent
| to the nature of garbage collected languages with mutable
| state. This is not an issue if you have more RAM then the
| system needs, but often RAM is extremely scarce.
| howinteresting wrote:
| A tool that requires "discipline" from its users is strictly
| worse than a tool that doesn't.
|
| I want to be able to write code without having to be
| "disciplined" about how I access memory. Means I can be more
| "disciplined" about business logic.
| sullyj3 wrote:
| C programmers like to talk about discipline, but no human is
| more disciplined than a compiler.
| creata wrote:
| > It's just ugly and looks like design by committee.
|
| I don't think it's ugly because it's design-by-committee, I
| think they intentionally made it ugly so that it's familiar to
| C++ people.
|
| > I also wonder why corporations are pushing Rust.
|
| You said it yourself: undisciplined people can't write C
| without introducing memory-related bugs, and it's much easier
| to hire undisciplined people than disciplined people.
|
| > It is an entirely different story if a project is started
| with sanitizers, Valgrind and best practices.
|
| Do you have an example of a project that is (a) built in such a
| way, (b) large, and (c) has a good track record on memory
| safety?
| ben0x539 wrote:
| C evangelism is exhausting too. Maybe we can stick to
| discussing the merits of each language instead of complaining
| about how people with differing opinions make us feel.
| up2isomorphism wrote:
| Actually I never see any occasion that a C guys jump into a
| well establish project and ask them to rewrite that in C.
|
| And TBH I rarely see other popular language did the similar
| things either, including very popular ones like python, Java
| or Go.
|
| And you even observe there is thing called "C evangelism"
| actually exists?
| ben0x539 wrote:
| Yeah? Check out any very public discussion of Rust and to a
| first approxiation there's always gonna be someone talking
| about how we should all just be using C instead. It's also
| not hard to find instances in open source projects of
| people ascribing ulterior motives or brain damage or
| ineptitude or whatever to anyone using another programming
| language.
|
| They don't call it C-lioning for nothing :^)
| mfru wrote:
| > It is an entirely different story if a project is started
| with sanitizers, Valgrind and best practices.
|
| What are the agreed upon tools and best practices in the C
| community as of right now?
| p0nce wrote:
| > I also wonder why corporations are pushing Rust.
|
| Recruiting.
| cjohansson wrote:
| Human-friendlyness and bug-prevention is very important, of
| course everthing in Rust can be created in C or Assembler och in
| machine-code but the question is how feasible is it that a
| typical human can do it? Rust has a lot of potential I think
| oblio wrote:
| Yeah, the sooner we move away from cowboy/Hero coding, the
| better. We could use a bit of humility in our field.
| _a1_ wrote:
| I appreciate the article, but it would be really nice if the
| author could add a timestamp to his blog posts. Without
| timestamps, it's impossible to know if any issue described in the
| article body still exists.
|
| I didn't read it, because it might present outdated knowledge.
| _a1_ wrote:
| The fact that my perfectly valid comment was down voted like
| this shows that HN has a pretty dysfunctional community. I
| think that is my last comment here ;)
| nindalf wrote:
| I read it. Didn't find any outdated information in it.
| teleforce wrote:
| Please check reply by dig1, it does contains some mis-
| information. It even incorrectly refer to the Heartbleed
| problem.
| nindalf wrote:
| dig1 is wrong. He uses the age old C defence of "it's not a
| problem with the language, it's just bad programmers
| programming badly". Apparently buffer reuse isn't a problem
| because "sane" libraries don't do it. Well, I'll believe it
| when we stop seeing security issues in C code bases.
| nyc_pizzadev wrote:
| Shouldn't this be Rust vs C++? C++ has a lot more parallels to
| Rust. Both are big, complex, and safe languages that can tuned
| for high performance. Infact, I would like to see more
| comparisons of Rust and C++ in the future.
| burntsushi wrote:
| No? I mean, if you're asking whether a Rust vs C++ comparison
| is useful, then sure, the answer is trivially true. If you're
| asking whether a Rust vs C++ comparison is _more useful_ than a
| Rust vs C comparison, then the answer is "maybe yes,
| depending." But certainly a Rust vs C comparison is useful on
| its own.
| pornel wrote:
| Author here: I'm a C programmer, who's replacing C with Rust.
| I've never liked C++ and never felt I fully get it. I've
| managed to fully grasp Rust though. I don't see that much
| similarity between Rust and C++ other than both use angle
| brackets for generic code and aspire to have zero-cost
| abstractions.
|
| C programming patterns have more-or-less equivalents in Rust.
| OTOH non-trivial C++ OOP or template usage is alien and hard to
| adapt to Rust.
|
| Rust has 1 (one) way to initialize an object. No constructors,
| initializer lists, or rules-of-<insert number>. Move semantics
| are built-in, without move/copy constructors/NRVO/moved-out-of
| state. No inheritance. No object truncation. Methods are
| regular function pointers. No SFINAE (generics are equivalent
| to concepts, and dumber, e.g. no variadic). Iterators require
| only implementing a single method. Operator overloading is all
| done in the style of the spaceship operator.
|
| It's not the same kind of complexity.
| not_knuth wrote:
| What a well-written and interesting piece that gets to the point!
|
| Compared to all the religious texts I've read about Rust, this is
| a huge breath of fresh air.
|
| Thanks for sharing! Bookmarking this.
| simias wrote:
| I completely agree with the points made here, it matches my
| experience as a C coder who went all-in on Rust.
|
| >"Clever" memory use is frowned upon in Rust. In C, anything
| goes. For example, in C I'd be tempted to reuse a buffer
| allocated for one purpose for another purpose later (a technique
| known as HEARTBLEED).
|
| Ha!
|
| >It's convenient to have fixed-size buffers for variable-size
| data (e.g. PATH_MAX) to avoid (re)allocation of growing buffers.
| Idiomatic Rust still gives a lot control over memory allocation,
| and can do basics like memory pools, combining multiple
| allocations into one, preallocating space, etc., but in general
| it steers users towards "boring" use or memory.
|
| Since I write a lot of memory-constrained embedded code this
| actually annoyed me a bit with Rust, but then I discovered the
| smallvec crate: https://docs.rs/smallvec/1.5.0/smallvec/
|
| Basically with it you can give your vectors a static (not on the
| heap) size, and it will automatically reallocate on the heap if
| it grows beyond that bound. It's the best of both world in my
| opinion: it lets you remove a whole lot of small useless allocs
| but you still have all the convenience and API of a normal Vec.
| It might also help slightly with performance by removing useless
| indirections.
|
| Unfortunately this doesn't help with Strings since they're a
| distinct type. There is a smallstring crate which uses the same
| optimization technique but it hasn't been updated in 4 years so I
| haven't dared use it.
| totalperspectiv wrote:
| I've been using smartstrings, which is both excellent and
| maintained. https://github.com/bodil/smartstring
| simias wrote:
| Ah, nice, I was looking at the smallstring package that's
| appears abandoned. I'll be sure to check this one out.
|
| The good thing about having a decent type system is that I
| expect that transitioning to smartstrings should be painless!
| Thank you for that.
| zesterer wrote:
| Um?? `smallstring` was updated 3 months ago.
| jancsika wrote:
| > alloca and C99 variable-length arrays
|
| I remember making an argument on a mailing list against using
| alloca on the grounds that there's usually a stack-blowing bug
| hiding behind it. As I revisited the few examples I remembered of
| it being used correctly, I strengthened my argument by finding
| more stack-blowing bugs hiding behind uses of alloca.
| [deleted]
| josephg wrote:
| A few years ago I hand ported a skip list implementation that
| used inlined dynamic arrays from C to rust. (Like, the last
| entry of the struct was a dynamically sized Foo[];). I needed a
| scattering of unsafe{} blocks and a bunch of tricks to make the
| resulting rust code equivalent to C, in order to prevent extra
| allocations + memory fragmentation on the rust side.
|
| When I ran my simple fuzz test in rust it seg faulted, crashing
| in 'safe' code. I thought for a moment there might be something
| wrong with the compiler (hahaha no). Sure enough, there was a
| bug in one of my far-too-clever unsafe blocks that was
| corrupting memory. Then that was in turn causing a crash later
| in the program's execution.
|
| That was one of my first big "aha" moments for rust - in rust
| because segfaults (should be) impossible in safe code, I only
| needed to study the code in my ~30 lines of unsafe code to find
| the bug. (Compared to 150+ lines of regular code). I had some
| similar bugs when I wrote the C version earlier, and they took
| all day to track down because in C memory corruption can come
| from anywhere.
| jandrewrogers wrote:
| As an observation, performance optimized code is almost always
| effectively single-threaded these days, even when using all the
| cores on a CPU to very efficiently process workloads. Given this,
| it is not clear to me that Rust actually buys much when it comes
| to parallel programming for the purposes of performance. Is there
| another reason to focus on parallelism aside from performance?
|
| This reminds me of when I use to write supercomputing codes. Lots
| of programming language nerds would wonder why we didn't use
| functional models to simplify concurrency and parallelism. Our
| code was typically old school C++ (FORTRAN was already falling
| out of use). The truth was that 1) the software architecture was
| explicitly single-threaded -- some of the first modern thread-
| per-core designs -- to maximize performance, obviating any
| concerns about mutability and concurrency and 2) the primary
| performance bottlenecks tended to be memory bandwidth, of which
| functional programming paradigms tend to be relatively wasteful
| compared to something like C++. Consequently, C++ was actually
| simpler and higher performance for massively parallel
| computation, counterintuitively.
| bumbada wrote:
| It really depends of your definition of terms. What do you call
| "performance optimized"?
|
| For example I consider glyph drawing as "performance
| optimized". It requires massive parallelism just to be able to
| display text smoothly in a high definition screen.
|
| But most people will never see it, because they use a library
| that they call that does all the work for them and do not need
| to care about that.
|
| The difference is tremendous. We are talking 100x more
| efficiency just using GPUs alone. You can get 1000x, 10.000x
| with hardware(electronic chip design) acceleration
| parallelism(increasing the cost and rigidity, and times to
| market too).
|
| It is so big that it is a different level. It is not
| performance alone. It is that some things are so inefficient
| that are just not practical(like expending a million dollars in
| your energy bill in order to solve a problem).
|
| Same happens with of course 3D, audio or video recognition.
| Sensor I/O. Artificial intelligence.
|
| Rust lets you just prototype lots of code in a parallel way in
| the CPU, even for things that will run in a FPGA or ASIC in the
| future. It let's you transition smaller steps:
| CPU->GPU->FPGA->ASIC
| trishume wrote:
| My impression is that what kind of parallelism patterns you
| need is pretty consistent within entire fields of programming.
| So you can go an entire career of performance optimization
| within HPC, game dev, film rendering or trading systems and
| never use the patterns the others say they use all the time.
|
| My experience with process-based parallelism is that yes on
| Linux it's basically isomorphic to thread-based parallelism.
| It's just so much more code to do the same thing.
|
| In Rust adding a new special-purpose background thread with
| some standard-library channels is 30 lines of code and I can
| probably even access the same logging system from the other
| thread.
|
| If I wanted to do that with processes I need to:
|
| - Coordinate a shared memory file over command line arguments
| or make sure everything is fork-safe
|
| - Find a library for shared-memory queues
|
| - Deal with making sure that if either process crashes the
| other process goes down with it in a reasonable way.
|
| - Make sure all my monitoring/logging is also hooked up to the
| other process.
|
| If I want to use a shared memory data-structure with atomics I
| need to either not use pointers or live dangerously and try and
| memory-map it at the exact same offsets in each process and
| ensure I use a special allocator for things in the shared file.
|
| Yes you can do all the same things with both approaches, I just
| find threads take way less code. It's not too bad if all your
| processes are doing the same thing, and you also need to scale
| to many servers anyhow. It's more annoying if you want to have
| a bunch of different types of special background processes.
| malkia wrote:
| The bigger issue is coordinating these threads ("workers")
| with threads from other processes, there is nothing on
| Windows and Linux to do so, then again I haven't had much
| experience with Grand Dispatch (OSX) to know if it's worth.
| Windows has new thread pool API, but even TBB or ConCRT do
| not use it. (though the new par-support in STL (msvc) does).
| johncolanduoni wrote:
| Windows, Linux and macOS all have inter-process mutexes and
| condition-variable-ish constructions. Windows has named
| mutex, semaphores, and events that can be opened by
| multiple processes, and the pthread API supports mutex and
| condition variables in shared memory. Linux additionally
| supports its futex primitive in shared memory regions
| (which is how the pthread API is implemented on that OS).
| sgtnoodle wrote:
| When you have built in support for threads in a language, it
| definitely makes sense that it would be easier to use than
| operating system mechanisms. For a lot of the non-embedded
| code that I end up writing, though, there's usually an
| inherent benefit to using processes over threads. It usually
| comes down to the benefits of having separate memory spaces.
| You can safely use code that was never written to be thread-
| safe, saving time otherwise spent refactoring gnarly old
| code. Also, it makes it a lot easier to mix and match
| different languages. For python in particular, it avoids
| having to battle for the global interpreter lock.
|
| I think what's nice about rust is that, because it makes it
| difficult to write thread-unsafe code, it's naturally easier
| to add threading at some point in the future without too much
| pain. As a result, more applications can benefit from having
| access to multiple CPU cores. I don't think that's quite the
| same thing as pure performance per watt, though. That really
| comes down to how the code was written, and how well the
| compiler can optimize it. Rust may have some advantages there
| over C, since it constrains what you can do so much that the
| compiler has a smaller state space to optimize over. Someone
| who knows what they're doing in C, though, could likely write
| very efficient code that effectively uses parallelism, and
| may gain an edge over rust simply by cleverly leveraging the
| relative lack of training wheels. For high performance
| compute, rust vs. C may be a wash. For consumer facing
| applications, though, the more programs that can use multiple
| cores to run faster (even if less efficiently), the better.
| amboar wrote:
| I realise your post is an argument in favour of Rust over C
| for these things, but regardless, you might be interested in
| a WIP library I've started to solve most of the issues you
| outlined: https://github.com/amboar/shmapper#libshmap
| jez wrote:
| > In Rust adding a new special-purpose background thread with
| some standard-library channels is 30 lines of code and I can
| probably even access the same logging system from the other
| thread.
|
| Do you happen to have a link to code that does this? This
| sounds similar to a problem I have right now and I'd love to
| see what solution you've arrived at.
| rational_indian wrote:
| > As an observation, performance optimized code is almost
| always effectively single-threaded these days, even when using
| all the cores on a CPU to very efficiently process workloads.
|
| Why?
|
| Edit: Thanks for all the replies. It seems this applies to
| data-parallel workloads only. I'd use a GPU for this. An RTX
| 3090 has around ~10000 CUDA cores (10000 simultaneous
| operations) v/s just ~10 for CPUs.
| mhh__ wrote:
| I think the issue is that the memory is the bottleneck in
| many applications (i.e. 2 loads per cycle, despite many more
| Functionanl units) and those workloads tend to be very non-
| embarassinglt parallel.
| tehjoker wrote:
| Sometimes your job has few or no inter-task dependencies and
| so there's no need to share between threads, but there's a
| heck of a lot of work that needs to be completed.
| hnuser123456 wrote:
| Essentially any significant task can be made multi threaded
| for any number of cores, it's just a lot of coding work.
| dllthomas wrote:
| There's a practical question of whether this is true for
| realistically encountered problems, with a sufficient
| threshold on both the size and utility of a "significant
| task" and with realistic numbers of cores.
|
| Without a requirement of utility it's easy to come up
| with counterexamples from math, eg "does the Collatz
| starting from Graham's number reach one?" Once you've
| exhausted cores that can be used for the actual
| arithmetic, you are still gated by the decision-making at
| each step so cores cannot work too far "ahead" of each
| other. There may well be much smarter things we can do
| than brute force, but that's not "just coding work" at
| that point.
|
| Theoretically, it doesn't hold - at some point you have
| split apart everything that can be split, and you are
| left with some essential chains of data dependency that
| cannot be further parallelized.
| jandrewrogers wrote:
| Data locality is everything for computational throughput.
| Having all data private to a single core is extraordinarily
| efficient compared to sharing data, and particularly mutable
| data, across cores.
|
| This creates a new problem: how do you balance load across
| cores? What if the workload is not evenly distributed across
| the data held by each core? Real workloads are like this!
| Fortunately, over the last decade, architectures and
| techniques for dynamic cross-core load shedding have become
| smooth and efficient while introducing negligible additional
| inter-core coordination. At this point, it is a mature way of
| designing extremely high throughput software.
| cwalv wrote:
| I'd like to learn more about the architectures and
| techniques developed over the last decade for this; can you
| recommend a few links or keywords to search?
| anonymousDan wrote:
| Can you point to any references/resources summarizing the
| latest cross-core dynamic load shedding techniques? Are
| they old techniques just now being applied in practice, or
| has something new been proposed?
| [deleted]
| oivey wrote:
| > As an observation, performance optimized code is almost
| always effectively single-threaded these days, even when using
| all the cores on a CPU to very efficiently process workloads.
|
| Not my experience at all. One big problem is that most
| languages in 2021 have very, very poor support for thread-based
| parallelism. It's crazy how many languages make it hard to do
| basic data parallel tasks. That steers people toward writing
| single threaded code and/or trying to rely on process-based
| parallelism which is basically strictly worse.
| jandrewrogers wrote:
| Parallelism in 2021 should not be tightly coupled across
| threads if performance matters, the limitations of that model
| are well-understood. There is no way to make that
| comparatively efficient; the CPU cache waste alone ensures
| that. Nothing you can do with thread support in a programming
| language will be competitive with e.g. a purpose-built
| scheduler + native coroutines. That's right up against the
| theoretical limit of what is possible in terms of throughput
| and it doesn't have any thread overhead. It does introduce
| the problem of load shedding across cores but that's solve
| for all practical purposes.
|
| I've been writing parallel code at the largest scales most of
| my career. The state-of-the-art architectures are all,
| effectively, single-threaded with latency-hiding. This model
| has a lot of mechanical sympathy with real silicon which is
| why it is used. It is also pleasantly simple in practice.
| CJefferson wrote:
| I don't understand -- isn't what you are suggesting single
| threaded async code? That might be useful for servers,
| where you are mostly waiting for other things (like
| databases and networks), but in ithe places the point of
| parallel is to get all your CPUs doing useful work, and
| then (in my experience, happy to be shown counterexamples),
| coroutines aren't very useful. You just want to blast a
| bunch of threads (or rightly coupled processes)
| jandrewrogers wrote:
| Yes, roughly single-threaded async, with each core
| running a disjoint subset of the workload on data private
| to that core. You can't beat the operation throughput.
| The software architecture challenge is shedding load
| between cores, since this _will_ hotspot under real
| workloads with a naive design. Fortunately, smoothly and
| dynamically shedding load across cores with minimal
| overhead and latency is a solved design problem.
|
| It works pretty well for ordinary heavy crunch code too.
| I originally designed code like this on supercomputers.
| You do need a practical mechanism for efficiently
| decomposing loads at a fine granularity but you rarely
| see applications that don't have this property that are
| also compute bound.
| CJefferson wrote:
| Ah, I understand now. I misinterpreted "no thread
| overhead" as meaning "I'm not running things in multiple
| threads", like the current node.js/javascript obsession,
| where we just run code in one thread and use a bunch of
| async to "parallelise". Sorry!
|
| I've (badly) written code like you describe -- usually by
| abusing fork to do my initial data structure setup, then
| using C pipes to pass jobs around. I suspect there are
| much better ways of doing it, but that parallelised well
| enough for the stuff I was doing. I'd be interested to
| know if there are good libraries (or best practices) for
| doing this kind of parallelism.
| oivey wrote:
| I was also confused. I agree that touching raw threads is
| usually not the right thing to do, and chains of parallel
| coroutines are one of the good abstractions. It's crazy
| how few languages have easy access to that very basic
| abstraction.
| truckerbill wrote:
| Do you know of any resources to learn more about this
| approach?
| andi999 wrote:
| Thanks. What is latency hiding?
| jandrewrogers wrote:
| There are many operations on data that are relatively
| slow from a CPU's perspective -- filling a cache line,
| page faulting, cache coherency, acquiring a lock, waiting
| on I/O to complete, etc. All of these add latency by
| stalling execution. In conventional software, when these
| events occur you simply stall execution, possibly
| triggering a context switch (which is very expensive). In
| many types of modern systems, these events are extremely
| frequent.
|
| Latency hiding is a technique where 1) most workloads are
| trivially decomposed into independent components that can
| be executed separately and 2) you can infer or directly
| determine when any particular operation will stall. There
| are many ways to execute these high latency operations in
| an asynchronous and non-blocking way such that you can
| immediate work on some other part of the workload. The
| "latency-hiding" part is that the CPU is rarely stalled,
| always switching to a part of the workload that is
| immediately runnable if possible so that the CPU is never
| stalled and always doing real, constructive work.
| Latency-hiding optimizes for throughput, maximizing
| utilization of the CPU, but potentially increasing the
| latency of specific sub-operations by virtue of
| reordering the execution schedule to "hide" the latency
| of operations that would stall the processor. For many
| workloads, the latency of the sub-operations doesn't
| matter, only the throughput of the total operation. The
| real advantage of latency-hiding architectures is that
| you can approach the theoretical IPC of the silicon in
| real software.
|
| There are exotic CPU architectures explicitly designed
| for latency hiding, mostly used in supercomputing.
| Cray/Tera MTA architecture is probably the canonical
| example as well as the original Xeon Phi. As a class,
| latency-hiding CPU architectures are sometimes referred
| to as "barrel processors". In the case of Cray MTA, the
| CPU can track 128 separate threads of execution in
| hardware and automatically switch to a thread that is
| immediately runnable at each clock cycle. Thread
| coordination is effectively "free". In software,
| switching between logical threads of execution is much
| more by inference but often sees huge gains in
| throughput. The only caveat is that you can't ignore tail
| latencies in the design -- a theoretically optimal
| latency-hiding architecture may defer execution of an
| operation indefinitely.
| spockz wrote:
| Is there some tool like ps/top /time that I can use
| measure how much "constructive" work my cpu spend doing?
| termie wrote:
| perf https://perf.wiki.kernel.org/index.php/Tutorial#Coun
| ting_wit...
| Const-me wrote:
| > There are exotic CPU architectures explicitly designed
| for latency hiding, mostly used in supercomputing.
|
| I don't know much about supercomputers, but what you
| described is precisely how all modern GPUs deal with VRAM
| latency. Each core runs multiple threads, the count is
| bound by resources used: the more registers and group
| shared memory a shader uses, the less threads of the
| shader can be scheduled on the same core. The GPU then
| switches threads instead of waiting for that latency.
|
| That's how GPUs can saturate their RAM bandwidth, which
| exceeds 500 GB/second in modern high-end GPUs.
| Jweb_Guru wrote:
| Latency hiding is a a way to substantially increase
| throughput by queuing massive numbers of requests or
| tasks while waiting on expensive resources (e.g. main
| memory access can have latency in the hundreds of cycles
| for GPUs). By scheduling enough tasks or enough requests
| that can be dispatched in parallel (or very soon after
| one another), after an initial delay you may be able to
| process the queued requests very quickly (possibly once
| per clock cycle or two) providing similar overall
| performance to running the same set of tasks sequentially
| with very low latency. However, such long pipelines are
| very prone to stalling, especially if there are data
| dependencies that prevent loads from being issued early,
| so getting maximum performance out of code on
| architecture that heavily exploits latency hiding
| techniques can require a lot of very specific domain
| knowledge.
| Jweb_Guru wrote:
| There is not a meaningful semantic difference between what
| you're describing and what tools like rayon provide (and
| BTW, threads do just fine when pinned to a core and
| appropriately managed as they should be in large data
| processing workloads). Whether threads are used on the
| backend is largely a distraction, you still have to write
| things roughly the same way to create correct code (for
| example, you cannot share memory between tasks on different
| cores, or on different nodes, without synchronizing
| somehow).
| otabdeveloper4 wrote:
| > purpose-built scheduler + native coroutines
|
| "Threads" are nothing but kernel-mode coroutines with
| purpose-built schedulers in the kernel.
|
| Redoing the same machinery except in usermode is _not_ the
| way to get performance.
|
| The problem is that scripting languages don't let you
| access the kernel API's cleanly due to various braindead
| design decisions - global locks in the garbage collector,
| etc.
|
| But the solution isn't to rewrite the kernel in every
| scripting language, the solution is to learn to make
| scripting languages that aren't braindead.
| eptcyka wrote:
| You are mistaking parallelism for concurrency.
| Galanwe wrote:
| > rely on process-based parallelism which is basically
| strictly worse.
|
| Why is that worse?
|
| I very seldomly use threads for concurrency, it creates
| monolithic binaries that are hard to maintain, configure, and
| understand.
|
| I much prefer a process based architecture with mmap'd shared
| memories for interprocess communications.
| snovv_crash wrote:
| Doesn't this just move the hard part, the maintenance,
| configuration and understanding, to a different (and
| equally complex) abstraction layer?
|
| Personally, I'd prefer to have a single binary that I start
| with some arguments, then need to also have a launch
| script, probably in a different language, which needs to
| coordinate all the starting, stopping, shared state etc.
|
| But, most of my work has been at the workstation level.
| Maybe it's different once you start needing clusters? The
| issue is that workstations have grown extremely powerful
| over the last few years with 50+ cores, 1000s of GPU
| 'cores', and hundreds of GB of memory. Clusters now bring
| the same headaches, in hardware, of multiprocess design.
| Galanwe wrote:
| > Doesn't this just move the hard part, the maintenance,
| configuration and understanding, to a different (and
| equally complex) abstraction layer?
|
| That's true indeed, but I find it more manageable that
| way.
|
| To me, managing independent processes instead of threads
| is especially powerful when the lifecycle of the
| concurrent work can vary.
|
| A typical example that happens quite often is when you
| have some kind of producer/consumer application. Say, you
| need to receive some data from a socket, and then process
| it.
|
| If implemented with threads, it becomes quite messy very
| fast. You end up with a big blob binary that spawns
| whatever the hell it wants and need to engineer some
| complex configuration file to tell it how many workers
| you want, etc. You also need some notification to this
| process to tell him to increase or decrease its amount of
| workers, etc.
|
| With independent process, it can be much more manageable.
| You can have a "collector" process that reads the socket
| and places messages in a shared memory, and if you want
| more/less workers, you just spawn worker processes to
| read from the shared memory.
| maccard wrote:
| You can make exactly this argument the other way around:
|
| If implemented with processes it becomes quite messy very
| fast. You end up with a bunch of processes that do
| whatever the hell they want, and need some complex
| orchestrator script to tell it how many processes you
| want, and need some notifications to the orchestrator to
| increase or decrease the number of workers
|
| With threads, you can have a main thread that reads the
| socket and places messages in a shared queue, and if you
| want more/less workers you just spawn worker threads to
| read from the queue.
| Galanwe wrote:
| > if you want more/less workers you just spawn worker
| threads to read from the queue.
|
| My point is precisely that's its not trivial to do that.
|
| You now need some signal mechanism to tell your main
| process to spawn or kill workers. That's an additional
| layer of complexity.
|
| Things get even worse if you want to add new types of
| workers that were unplanned.
|
| Say I want a new type of worker that is just a forwarder,
| or a new type of worker that stores the queue on disk for
| replaying it.
|
| With a monolithic thread based design, you now need to
| stop your whole main and worker threads and start a newly
| built process that support these workers.
|
| With a process based design, you just spawn a new worker
| process and point it to the existing shared memory
| without any interruption.
| memco wrote:
| I was really struck by a comment Jonathan Blow made on stream
| recently: he said he's never written a parallel for loop in
| his whole career. I seem to recall the implication being that
| they're often not really necessary for performant code.
| There's also been some discussion lately about issues with
| asynchronous code both in Rust and Python. Point being that
| parallelism still had a ways to go before it's proven it's
| usefulness. However, I agree with you that it would be nice
| to see more language tootling to make it simpler since i work
| on some bits of code that I think could benefit from
| parallelization but the amount of work I'd have to put in
| mean it's a very low priority given the savings.
| CJefferson wrote:
| The world is full of highly parallel programs getting
| useful work done. Most graphics, AI and compression
| libraries (picking 3 easy examples I've worked on)
| parallelize well, and can usually make use of all the cores
| you can throw at them.
|
| Jonathan Blow makes good games, but chooses not to make
| particularly CPU intensive ones. That's fine, but that's
| also his choice.
| mariusor wrote:
| He's also currently building one of the fastest compilers
| around. It's unreasonable to consider that he never
| encountered use cases where parallelism makes sense.
| memco wrote:
| Indeed, he wasn't saying parallelism is not useful, just
| that the specific construct of a parallel for loop was
| not in his wheelhouse for certain reasons.
| mariusor wrote:
| My impression of Jon's work is that he requires low
| enough level access to his hardware so he's the one that
| makes decisions about where and what runs. Language level
| parallel for is definitely not that. :D
| snovv_crash wrote:
| Depending on the language, it can be as easy as adding
| #pragma omp parallel for
|
| And if this loop is your bottleneck, you can get almost
| perfect scaling with cores.
| pixel_fcker wrote:
| This says much more about the type of programs Jonathan
| Blow tends to write than anything else.
| howinteresting wrote:
| I wrote a parallel iteration (map-reduce) last week in some
| CPU-heavy code, took 5 minutes with Rayon. Sped my code up
| by around 10x on a 12-core machine, example benchmark going
| from 7 seconds to 700 milliseconds. It's serious business.
| Jweb_Guru wrote:
| Parallelism and asynchronous code are not the same, and in
| the case of Rust they are _very much_ not the same.
| Parallel for provides massive advantages for many things
| including game programming (from experience) so with all
| due respect I think this says more about Jonathan Blow than
| it does anything about "parallelism still needing to prove
| itself."
| roca wrote:
| I never wrote a parallel for-loop in 15 years working on
| Firefox, because it's hard in C++, it's risky and difficult
| to maintain the thread-safety invariants, and it's not all
| that useful in most parts of the browser.
|
| I write them quite often in Rust, because Rayon makes it
| super easy, there is almost no risk because the compiler
| checks the relevant thread-safety invariants, and I'm
| working on different problems where data parallelism is
| much more useful.
| snovv_crash wrote:
| I've used them extensively in C++. Doing it manually by
| managing your own threads is a pain, but simple OpenMP
| based parallel loops work really well, and also supports
| tasks like building vectors and simple reductions.
| roca wrote:
| When your loop body uses complex library APIs over
| complex data it's still hard to be confident in C++ that
| everything's threadsafe and you're avoiding data races.
|
| Maybe it's not so hard if you're in a domain like HPC
| where the libraries you use are designed specifically to
| be used with data parallelism. But when you're pulling
| together code from different sources that may or may not
| have been used in an aggressively parallel application
| before...
| IgorPartola wrote:
| In what way is process based parallelism strictly worse? On
| Linux a process and a thread in the kernel are both the same
| thing. The main difference between the two is that threads
| share memory by default whereas a process would need to
| explicitly mmap a chunk of memory to share with another
| process. This means that with threads you get to save RAM
| because of application code not having to take up more space
| (except memory is effectively deduplicated already because
| those are read only blocks) and with threads you have to
| explicitly guard against trampling over each others' memory,
| whereas with processes it's safety by default and you have to
| make an explicit choice to share memory, limiting the number
| of places you can forget to add or check a lock. I will grant
| you that languages where standard libraries by default
| shoehorn you into using sockets to communicate between
| processes do by default introduce more overhead than just
| using threads. But any serious language will have at least
| one queue implementation that's based on very fast primitives
| over shared virtual memory. I say this as someone whose done
| projects using a number of different types of parallelism,
| including process and thread, and can find strengths for all
| these. I just think treatment of process based parallelism is
| undeserved: it can be very efficient if done well, and no
| worse than a lot of the other methods for a large number of
| use cases.
| Shoop wrote:
| I do not think your model of threads and processes is
| correct. Processes have different address spaces whereas
| threads share an address space. Context switching between
| threads is much cheaper than context switching between
| processes because you do not have to swap page tables and
| do a tlb flush. tlb flushes are extremely expensive. I also
| think you are misunderstanding how mmap works. mmap is not
| related to thread spawning.
| mypalmike wrote:
| Not the parent, but... mmap is related to thread spawning
| (well, process forking) in that using the MAP_SHARED flag
| will result in a pointer that is valid shared memory for
| both the parent process and any forked processes.
| gmueckl wrote:
| If you stick to one process per core, the number of TLB
| flushes doesn't change. You can set processor affinity to
| make sure of that. If you create more threads/processes
| than cores, you might be able to get measurable impact.
|
| I don't understand your comment about mmap. It is often
| used to share memory between related processes.
| oivey wrote:
| As pointed out in a sibling, processes have their own
| address spaces and aren't as cheap to spawn as threads. I
| write code involving shared memory. It's usable, but it
| also a pretty big pain to get right. It also significantly
| complicates things like managing memory ownership.
| icedchai wrote:
| Some languages make it very simple to do parallel operations.
| Java and Scala parallel streams, for example, are like one
| line of code. Obviously that won't work for all use cases,
| but when it does, it is simple.
|
| Heavily multi threaded code is difficult to write correctly.
| Do it wrong, you wind up with race conditions, data
| corruption, dead locks because a thread pool or other
| resource is exhausted, thread leaks because you didn't shut
| something down correctly. The problems go on and on.
| snovv_crash wrote:
| Use a map-reduce style pattern for your work allocation and
| you lose some small amount of efficiency, but the design
| becomes much much easier. You can even mix and match
| different types of work in your reduce stage to keep the
| different types of hardware busy.
| _ph_ wrote:
| There are a few tasks, where process-based parallelism is
| working fine. Like a forking server for handling network
| requests. This is quite efficient and works even if the
| handling function is not thread save. Obviously, you get
| memory safety as well.
|
| Unfortunately, that is only a certain subsection of problems
| and usually you want to be able to use parallel computations
| on the function call level. There the support for parallel
| computations of Rust or Go shines. When at each point in the
| program flow you can decide to go parallel.
| dnautics wrote:
| It's not a bandwidth issue.
|
| Functional programming (especially, say, actor systems) is
| better for organizing mental models of concurrency when your
| concurrency is coupled with communication between the
| components. For hpc, you're typically optimizing for gustafson
| scaling (versus amdhal scaling) where you are running multiple
| copies of the same, computationally costly linearly organized
| code with no coupling between instances except statistical
| aggregation of results, so there is no particular benefit to
| functional-style concurrency.
|
| (And some FPLs,like Julia, are perfectly good at hpc anyways)
| jandrewrogers wrote:
| FWIW, most supercomputing looks nothing like map-reduce; only
| the most trivial problems look like that. In a data model
| sense, a lot of supercomputing is join operation intensive,
| hence why they spend big bucks on high-bandwidth low-latency
| interconnects. STREAM benchmarks were more predictive of
| real-world supercomputing code performance than LAPACK in the
| majority of case 15+ years ago and it became more biased
| toward the former with time.
|
| The codes I worked on were complex graph analysis,
| spatiotemporal behavioral analysis, a bit of geospatial
| environmental modeling, and in prehistoric times thermal and
| mass transport modeling. These codes (pretty much anything
| involving reality) are intrinsically tightly coupled across
| compute nodes. Low-latency interconnects eventually gave way
| to latency-hiding software architectures but at no point did
| we use map-reduce as that would have been insanely
| inefficient given the sparsity and unpredictability of the
| interactions between nodes.
|
| These were the prototype software architectures for later
| high-performance databases. Every core is handling thousands
| or millions of independent shards of the larger computational
| model, which makes latency-hiding particularly efficient.
| CraigJPerry wrote:
| I have nothing to add, just wanted to say that reading all
| your comments this morning has been fascinating and
| educational. I've really enjoyed it so Thanks for sharing.
| This kind of expert insight is one of the reasons i visit
| HN.
| physicsguy wrote:
| This is similar to my experience too. If people can write
| out a single Python function and apply it to all of a large
| amount of data then great, but that isn't the majority of
| supercomputing programming.
| dnautics wrote:
| I've done this across 10,000 cores, and helped people do
| similar tasks as well, I guess my experience is not
| normal. But IIRC even for things that, say use MPI to
| coordinate cores in the small you want to repeat across
| many cores, limit the spread across cores, limit your
| blocking coordination, lest you wind up with the old joke
| that "HPC is the art of turning a CPU-bound job into an
| I/O bound job".
| _ph_ wrote:
| When we talk about support for threading/concurrent programming
| in programming languages, it is less about how to reach the
| theoretical limits of your system best, especially if you are
| free to architect the whole software stack towards that goal.
| In that case, your statements might apply.
|
| It is about how easily a programmer, who deals with a certain
| subtask in a system, can utilize more cores for the this task.
| Not talking about supercomputing, but looking at a smarktphone
| or a typical PC. There you usually have most cores just idle
| unused, but if the user triggers an action, you want to be able
| to use as many cores as it speeds up computation. Language
| support for parallelism makes a huge difference there. In Go I
| can write a function to do a certain computation and quite
| often it is trivial to spread several calls across goroutines.
| jorangreef wrote:
| You are not factoring in the cost of context switches, and
| that many user applications today are memory-bound and not
| CPU-bound.
|
| It's one of the secrets exploited by the M1 chip, seen in how
| many more cache lines the CPU's LFB can fill concurrently
| compared to Intel chips and that these are now 128 byte cache
| lines instead of 64 byte cache lines.
| _ph_ wrote:
| Which context switches? With the Go model, I have exactly
| one thread per CPU, no context switches. And if you are
| memory-bound, why have more CPUs?
|
| But sure, there is a reason why the M1 has so stellar
| performance, it has one of the fastest single-thread
| performances and many applications do not manage to load
| more than 4 cores for common tasks - which partially is
| also a consequence of doing that is difficult in many
| programming languages, but easy in some, which are only
| slowly gaining traction.
| gameswithgo wrote:
| >As an observation, performance optimized code is almost always
| effectively single-threaded these days
|
| I do not accept this premise. Things are increasingly
| multithreaded.
| antiquark wrote:
| Yeah but, C is essentially 32 years old by now.
|
| A more useful comparison would be to modern C++.
| nindalf wrote:
| I think it's a reasonable comparison. C is still a language
| that is widely used. In some niches, it is the only acceptable
| language. Comparing C with Rust is useful for people in those
| niches. An example of this is the Linux kernel.
| w-m wrote:
| Actually I t's even older. I know it's not an official
| standard, but most if not all points on C in the article would
| also apply to K&R C. The book was published in 1978, more than
| 40 years ago.
| ReactiveJelly wrote:
| I'm a Rust evangelist, but the article is titled "Speed of Rust
| vs. C" and doesn't seem to contain even one benchmark.
|
| For fuck's sake.
| tazjin wrote:
| In my opinion, the level of detail in this article is much more
| useful than small benchmarks of code that doesn't resemble real
| applications anyways.
| zesterer wrote:
| Benchmarks wouldn't tell the whole story. This detailed writeup
| is far better in that it gives information about _how_ and
| _where_ the two languages differ.
| pornel wrote:
| There's already The Benchmarks Game and ixy-languages if you
| want hard numbers.
|
| Maximum speeds are already explored. I wanted to discuss an
| aspect that's not typically covered by pure benchmarks: what
| can you expect from normal day-to-day use of these languages.
| Not fine-tuned hot loops, but a "median" you can expect when
| you just need to get shit done.
|
| If I tried to write a benchmark code to represent average,
| practical, idiomatic, but less-than-maximally optimized code, I
| don't think anyone would believe me that's a fair comparison.
| So I describe problems and patterns instead, and leave it to
| readers to judge how much applies to their problems and
| programming style.
| igouy wrote:
| > Maximum speeds are already explored.
|
| Also sub-maximum speeds -- start at the bottom of the
| measurements and work up from the 5.37s g++ program to the
| 0.72s g++ program :-)
|
| https://benchmarksgame-
| team.pages.debian.net/benchmarksgame/...
| howinteresting wrote:
| Thank you for writing this. Real world, qualitative
| experience reports are vital.
|
| My experience using Rust vs C aligns with yours as well.
| up2isomorphism wrote:
| The article talks way too high level and is written like a
| marketing people even the title sounds technical, for example:
|
| "Rust enforces thread-safety of all code and data, even in 3rd
| party libraries, even if authors of that code didn't pay
| attention to thread safety. Everything either upholds specific
| thread-safety guarantees, or won't be allowed to be used across
| threads."
| burntsushi wrote:
| That doesn't sound too high level to me. Maybe a small quibble
| is the definition of "thread safety," but a reasonable one
| would be, "no undefined behavior in the presence of
| simultaneous access." In other words, no data races. And that's
| absolutely true and consistent with Rust's definition of
| safety. Another small quibble might be that, "even if the
| authors of that code didn't pay attention to thread safety _and
| didn 't use 'unsafe'_" would be more precise.
|
| It's not marketing speak.
| up2isomorphism wrote:
| There is simply no way you can enforce "thread safety on ALL
| data", unless you pay unreasonable amount of synchronization
| costs, which in that case, is a trivial thing to accomplish.
|
| This is as same as some one tell you that you will never
| loose any money by investing a certain asset.
| howinteresting wrote:
| Rust is a constructive proof that your assertion is simply
| false. It comes at the cost of some complexity--every Rust
| type carries thread-safety information with it--but the
| benefit is that writing correct parallel Rust code becomes
| very easy.
|
| What you cannot easily do in Rust is _dynamically_ switch
| thread safety on or off.
| steveklabnik wrote:
| How well do you know Rust, and how it works, and what it
| guarantees?
|
| Like, do you have a specific objection to the way Rust
| accomplishes this?
| up2isomorphism wrote:
| "thread safety for all data" is not even a well-defined
| term, I do not know what you are trying to argue about.
| burntsushi wrote:
| I gave you a definition in my initial reply to you.
| [deleted]
| pornel wrote:
| But this is true. I mean specifically about Send and Sync
| traits that have to be implemented on types for the compiler to
| allow them in multi-threaded constructs, like `thread::spawn`
| or Rayon's parallel iterators.
|
| If you write a library, and use e.g. thread-unsafe `Rc` or not-
| sure-if-safe raw pointers anywhere in your structs, the
| compiler will stop _me_ from using your library in my threaded
| code.
|
| This is based on a real experience. I've written a single
| threaded batch-processing code, and then tried to make it
| parallel. The compiler told me that I used a GitHub client,
| which used an HTTP client, which used an I/O runtime, which in
| this configuration stored shared state in an object without a
| Mutex. Rust pointed out exactly the field in 3rd party code
| that would cause a data race. At compile time.
| pabs3 wrote:
| FTR, there are some efforts to integrate GCC & Rust:
|
| https://github.com/antoyo/rustc_codegen_gcc
| https://github.com/Rust-GCC/gccrs https://github.com/sapir/gcc-
| rust/
| ackxolotl wrote:
| We've implemented network drivers in C and Rust and did a
| performance comparison. Interestingly, the C-to-Rust-transpiled
| code ended up being faster than the original C implementation:
| https://github.com/ixy-languages/ixy-languages/blob/master/R...
| jmacjmac wrote:
| https://github.com/emmericp/ixy/blob/0e00605be4153b06df06184...
|
| Looks like you're compiling C code with -O2. Does Rust build
| set -O3 on clang? Did you try -O3 with C? I know it's not
| guaranteed to be faster, just curious.
| Shadonototro wrote:
| Good catch
| dralley wrote:
| It looks like the answer is "yes"
|
| https://doc.rust-
| lang.org/cargo/reference/profiles.html#rele...
| jmacjmac wrote:
| Then a fair benchmark would be compiling C code with clang
| -O3 :)
| vlmutolo wrote:
| > "Clever" memory use is frowned upon in Rust. In C, anything
| goes. For example, in C I'd be tempted to reuse a buffer
| allocated for one purpose for another purpose later (a technique
| known as HEARTBLEED).
|
| This made me laugh
| secondcoming wrote:
| It was quite funny but it's quite likely you'll be reusing
| memory anyway whether it's on the stack or the heap, no?
|
| The issue with this is that 'clever' compilers can optimise out
| any memset calls you do.
| pornel wrote:
| Rust's safety rules also forbid access to uninitialized
| memory, even if it's just a basic array of bytes. This is an
| extra protection against accidentally disclosing data from a
| previous "recycled" allocation.
| viraptor wrote:
| It's not trivial to write a funny and clever burn, but this
| just hits the spot...
| waterhouse wrote:
| That is nice, although I think Heartbleed was due to a
| missing bounds check enabling the reading of adjacent memory,
| not due to reusing the same buffer...
| gameswithgo wrote:
| iirc both issues caused the problem. Buffer overlow let the
| memory get read, re-use meant there was important data in
| the buffer.
| NobodyNada wrote:
| If my memory is correct: yes, the root cause was a missing
| bounds check, but the vulnerability was _much_ worse than
| it could have been because OpenSSL tended to allocate small
| blocks of memory and aggressively reuse them -- meaning the
| exploited buffer was very likely to be close in proximity
| to sensitive information.
|
| I don't have time right now to research the full details,
| but the Wikipedia article gives a clue:
|
| > Theo de Raadt, founder and leader of the OpenBSD and
| OpenSSH projects, has criticized the OpenSSL developers for
| writing their own memory management routines and thereby,
| he claims, circumventing OpenBSD C standard library exploit
| countermeasures, saying "OpenSSL is not developed by a
| responsible team." Following Heartbleed's disclosure,
| members of the OpenBSD project forked OpenSSL into
| LibreSSL.
| waterhouse wrote:
| Ah, good point; at the very least things like zeroing out
| buffers upon deallocation would have helped. Yes, I was a
| fan of the commits showing up at opensslrampage.org. One
| of the highlights was when they found it would use
| private keys as an entropy source:
| https://opensslrampage.org/post/83007010531/well-even-if-
| tim...
| saagarjha wrote:
| Until very recently, memory allocators were more than
| happy to return you the thing you just deallocated if you
| asked for another allocation of the same size. It makes
| sense, too: if you're calling malloc/free in a loop,
| which is pretty common, this is pretty much the best
| thing you can do for performance. Countless heap exploits
| later (mostly attacking heap metadata rather than stale
| data, to be honest) allocators have begun to realize that
| predictable allocation patterns might not be the best
| idea, so they're starting to move away from this.
| rcxdude wrote:
| True of the more common ones, but it should be
| acknowledged that OpenBSD was doing this kind of thing
| (and many other hardening techniques) before heartbleed,
| which was the main reason Theo de Raadt was so upset that
| they decided to circumvent this, because OpenBSD's
| allocator could have mitigated the impact otherwise.
| loeg wrote:
| Even higher-performance mallocs like jemalloc had heap
| debugging features (poisoning freed memory) before
| Heartbleed, which -- if enabled -- would catch use-after-
| frees, so long as libraries and applications didn't
| circumvent malloc like OpenSSL did (and Python still does
| AFAIK).
| supergarfield wrote:
| > and Python still does AFAIK
|
| Don't you sort of have to do that if you're writing your
| own garbage collector, though? I guess for a simple
| collector you _could_ maintain lists of allocated objects
| separately, but precisely controlling where the memory is
| allocated is important for any kind of performant
| implementation.
| loeg wrote:
| Python does refcount-based memory management. It's not a
| GC design. You don't have to retain objects in an
| internal linked list when the refcount drops to zero, but
| CPython does, purely as a performance optimization.
|
| Type-specific free lists (just a few examples; there are
| more):
|
| * https://github.com/python/cpython/blob/master/Objects/f
| loato...
|
| * https://github.com/python/cpython/blob/master/Objects/t
| upleo...
|
| And also just wrapping malloc in general. There's no
| refcounting reason for this, they just assume system
| malloc is slow (which might be true, for glibc) and wrap
| it in the default build configuration:
|
| https://github.com/python/cpython/blob/master/Objects/obm
| all...
|
| So many layers of wrapping malloc, just because system
| allocators were slow in 2000. Defeats free() poisoning
| and ASAN. obmalloc can be disabled by turning off
| PYMALLOC, but that doesn't disable the per-type freelists
| IIRC. And PYMALLOC is enabled by default.
| fulafel wrote:
| That's what happens by using normal malloc/free anyway,
| no? Implementations of malloc have a strong performance
| incentive to allocate from the cache hot most recently
| freed blocks.
| ric129 wrote:
| Yes, all allocators (except perhaps OpenBSDs from what I
| see in this thread) do this. It is also why `calloc`
| exists - because zero-initializing every single
| allocation is really, really expensive.
| eps wrote:
| That's not a good burn though.
| Blikkentrekker wrote:
| It's incorrect, however.
|
| _Heartbleed_ wasn 't caused by reusing buffers; it was
| caused by not properly sanitizing the length of the buffer
| from entrusted input, and reading over it's allocated size,
| thus allowing the attacker to read into memory that wasn't
| meant for him.
| pornel wrote:
| OpenSSL had its own memory-recycling allocator, which made
| the bug guarantee leaking OpenSSL's own data. Of course
| leaking random process memory wouldn't be safe either, but
| the custom allocator added that extra touch.
| sivizius wrote:
| This was actually a somewhat significant reason I shared this
| article. (^.^)
| gridspy wrote:
| > in C I'd be tempted to reuse a buffer allocated for one
| purpose
|
| ... In rust I'd just declare an enum for this. Enums in Rust
| can store data. In this way they are like a safe union.
| docmars wrote:
| Awesome, now do charts! ;)
___________________________________________________________________
(page generated 2021-03-13 23:03 UTC)