Hacker News

Top stories

Live mirror
30 storiesupdated just nowView source snapshot
  1. F-Droid 2.0 (f-droid.org)
    201comments
  2. Show HN: Make cursed fonts like Times New Bastard (mitpit.com)
    41comments
  3. Show HN: Whiteboard (YC W26) – An open-source IDE for thoughtful software design (github.com/devdotfast)
    51comments
  4. Fearless SIMD v1.0 (linebender.org)
    18comments
  5. Rails World 2026 Opening Keynote [video] (youtube.com)
    141comments
  6. My weird new hobby: Wandering around Tokyo on Google Maps (ahmedhossamdev.com)
    48comments
  7. Using LLMs to trace alchemical knowledge and decode 17th century letters (resobscura.substack.com)
    4comments
  8. Book review: Is parallel programming hard, and, if so, what can you do about it? (ahelwer.ca)
    14comments
  9. The forgotten battle of East Lansing (eastlansinginfo.news)
    8comments
  10. Sourcehut account takeover via build logs (XSS in ansi2html) (blog.arusekk.pl)
    1comments
  11. Stable (YC W20) Is Hiring Product Engineers (usestable.com)
    —discuss
  12. Why is the liver so weirdly regenerative? (dynomight.substack.com)
    91comments
  13. Google’s Project Suncatcher to put ML infrastructure in space (blog.google)
    98comments
  14. Forging 1024-bit RSA signatures in nearly SNFS time [pdf] (iacr.org)
    5comments
  15. Two-tier encryption in the UK (macanorak.com)
    344comments
  16. Geothermal heat map of US hot springs (soakingsprings.com)
    20comments
  17. Opus 5.5 is good at explainer videos (launchvideo.io)
    1comments
  18. The Board Game of the Alpha Nerds (2014) (grantland.com)
    1comments
  19. WaveDigger: Dig into wireless signals to discover their physical locations (github.com/christianrowlands)
    11comments
  20. Creatine uptake enhances antitumor immunity (cell.com)
    145comments
  21. Web-based IBM 1620 emulator and IPL-V from 1963 (github.com/pkimpel)
    11comments
  22. Early rogue AI agent activity and attempts to hack found on urlquery.net (transluce.org)
    197comments
  23. August 27 TCRF DDoS Attack Postmortem (xkeeper.net)
    1comments
  24. Security auditing in the age of (good enough) AI (trailofbits.com)
    1comments
  25. A Million Agents Is a Distributed System Problem (instacloud.com)
    5comments
  26. Show HN: Treepeat – Code similarity detection using Tree-sitter (github.com/dsummersl)
    1comments
  27. 2DWillNeverDie (2dwillneverdie.com)
    —discuss
  28. Nokia Design Archive (2025) (aalto.fi)
    109comments
  29. Show HN: AgentRun: DSL to turn agents into workflows (github.com/parcha-ai)
    3comments
  30. Motor Characterization for Small Running Robots (2016) (robot-daycare.com)
    —discuss

Towards fearless SIMD, 7 years later

177 pointsby 1y agolinebender.org
175 comments
1y agoHN ↗

Very interesting! I posted a vector and quaternion lib here a few weeks ago, and got great feedback on the state of SIMD on these things. I since have went on a deep dive, and implemented wrapper types in a similar way to this library. Used macros to keep repetition down. Split into three main sections:

  - Floating point primitives. Like this lib. Basically, copied `core::simd`'s API. Will delete this part once core::simd is stable. `f32x8`, `f64x4` types etc, with standard operator overloads, and utility methods like `splat`, `to_array` etc.

  - Vec and Quaternion analogs. Same idea, similar API. Vec3x8, Quaternionx8 etc.

  - Code to convert slices of floating point values, or non-SIMD vectors and quaternions to SIMD ones, including (partial) handling of accessing valid lanes in the last chunk.

I've incorporated these `x8` types into a WIP molecular dynamics application; relatively painless after setting up the infra. Would love to try `Vec3x16` etc, but 512-bit types aren't stable yet. But from Github activity on Rust, it sounds like this is right around the corner!

Of note, as others pointed out in the thread here I mentioned, the other vector etc libs are using the AoS approach, where a single f32x4 value etc is used to represent a Vec3 etc. While with this SoA approach, a `Vec3x8` is for performing operations on 8 Vec3s at once.

The article had interesting and surprising points on AVX-512 (Needed for f32x16, Vec3x16 etc). Not sure of the implications of exposing this in a public library is, i.e. might be a trap if the user isn't on one of the AMD Zen CPUs mentioned.

From a few examples, I seem to get 2-4x speedup from using the x8 intrinsics, over scalar (non-SIMD) operations.

1y agoHN ↗

Why do the x4/x8 types seem to be the default in rust?

A portable SIMD feature should encurage portable SIMD and not a specific vector register size.

1y agoHN ↗

This sounds like a great idea. I went with this approach because I'm new to SIMD, so I aped the most promising API (core::simd), extending it naturally.

I need to think through the consequences. It might involve feature gates, and/or an enum. So, for example, instead of:

  pub struct f32x8(__m256);

It might be this internally, with some method to auto-choose variant based on system capability?:

  pub enum f32_simd {
    X8(__m256),
    X16(__m512), 
  }

etc. Thoughts?

1y agoHN ↗

I'm not proficient in Rust, but API wise I'd conceptually define types like f32xn or f32s, which have the number of elements that fit into a vector register for your target architecture, so 4 for NEON/SSE, 8 for AVX and 16 for AVX512.

I can recommens lookig at the highway library: https://github.com/google/highway

1y agoHN ↗

Amen! I really do not understand this. It has been 7 years since SVE was introduced.

Writing an application in terms of a specific lane count loses performance portability - either it's too many, or too few, for the particular CPU. And it also enables/encourages antipatterns like putting RGB in Vec4.

1y agoHN ↗

I’ve said it before and I’ll say it again: Rust feels like a Python developer’s idea of a high-performance computing language. It’s a great language for many kinds of applications — just not when you need to squeeze out every bit of performance from advanced hardware.

Even before getting into SIMD, try using Rust for concurrent, succinct, or external-memory data structures. It quickly becomes clear where the friction is.

Cargo is fantastic — clean, ergonomic, and a joy compared to many toolchains. But it’s much easier to keep things simple when you don’t have to support dozens of AVX-512 variants, AMX, SME, different CUDA generations, ROCm, or any of the other modern hardware capabilities.

Standardising SIMD in the standard library — in Rust or C++ — has always been a questionable idea. Most of these APIs cater to operations that compilers already auto-vectorize reasonably well, and they barely touch the recent capabilities of SIMD. Just consider how hard it is to build any meaningful abstraction over the predicate/register models across AVX-512, SVE, and RVV.

RVV aside, this should illustrate the point: https://www.modular.com/blog/understanding-simd-infinite-com...

1y agoHN ↗

I guess it comes down to application. If you don't attempt to find the most general solution, you can dodge those pitfalls. Case in point, abstracting over AVX-512, SVE, and RVV may be tough, but picking one is fine (On nightly only for now), can with the right abstractions can be almost as straightforward as using normal scalar values. I don't have a solution on the CUDA variants either; have been hard-coding that as well... (Cudarc lib with CUDA-version feature gates and GPU-series-specific code). Haven't hit a brick wall yet, but might... or might not.

1y agoHN ↗

Just consider how hard it is to build any meaningful abstraction over the predicate/register models across AVX-512, SVE, and RVV.

Note that Highway mentioned in the post does take care of this, which is no easy feat but also a proof that it is doable.

1y agoHN ↗

I don't think Rust is particularly problematic here. As long as you don't want to do funky things like use immutable argument memory as temporary scratch space (with you restoring the values afterwards of course), all it means is some `unsafe`ing at worst, compared to C/C++. And there are some safe abstractions you can make over loads/stores (everything else being safe, even if not yet marked as such).

Do agree that a standard SIMD type is rather pointless, if not immediately, then in like 5 years. (and, seemingly, both Rust and C++ are like over 10 years behind on SIMD, so they're already out-of-date)

Maybe somewhat useful if you just want the simple ~8x speedup, and not squeeze out the last 1.4x or whatever, but autovectorization should be capable of covering a significant amount of such.

1y agoHN ↗

… except for byte-level processing, variable-length codecs, or mixed-precision numerics. That never works with autovectorization and can’t be solved with general-purpose SIMD wrappers. For me the solution was to implement those manually, and even at a scale of just 2 libraries I’ve eneded up with somewhat different project layouts & dispatch mechanisms: https://github.com/ashvardanian/SimSIMD , https://github.com/ashvardanian/StringZilla

One big family not covered there, is sparse data-strictures and related algorithms. I’ve only started integrating scatter/gather in AVX-512 and SVE, and on synthetic benchmarks both look promising: https://github.com/ashvardanian/less_slow.cpp/releases/tag/v...

Those should probably unlock a much wider set of applications for SIMD, but designing libraries for those may benefit from yet another project structure.

1y agoHN ↗

Ah yeah, gather/scatter are indeed a rather problematic thing for autovectorization. That said, with no-alias info (which Rust has a lot of) it's possible: https://rust.godbolt.org/z/zTfo9nxhd.

Unfortunately it doesn't get autovectorized without the unsafes, but theoretically it should be possible-ish for bounds checking to be autovectorized (most problematic aspect being that it might be hard to annoying-to-impossible to ensure that in the case of multiple panic/UB sources the proper one happens first).

I'd imagine in any non-trivial situation you'd want a custom layer of abstractions over whatever the language provides for all languages. For that a portable-simd thing is actually a rather good base, on which you could add custom arch-specific abstractions/ops as desired.

Not sure what's problematic with mixed-precision (I know SVE is rather weird for mixed-width elements, but that's about it?), though I primarily don't care about float stuff generally. Also no clue what's problematic with byte-level stuff.

Indeed there are still a bunch of things that you want proper manual SIMD for (hell the SIMDful project I work on has an entire DSL for doing nice SIMD), but autovectorization still covers a good amount.

1y agoHN ↗

except for byte-level processing, variable-length codecs, or mixed-precision numerics. That never works with autovectorization and can’t be solved with general-purpose SIMD wrappers.

Counterexamples: Chromium's byte-level HTML scanning, several var-len bit packing codecs, and Gemma.cpp's matmul is mixed-precision (fp8->bf16->fp32->fp64). All written with the Highway general-purpose SIMD wrapper. Please revise your post or expand upon the structure/dispatch concern.

1y agoHN ↗

Interesting references! I remember that Gemma.cpp used Highway, but I haven't checked the others much.

Here is a puzzle, then. Let's say we are checking a single register of bytes for element-wise equality with another register of the same size. In AVX2, the output is another YMM register of 0xFF or 0x00 values. In AVX-512, for full ZMM-wide comparisons, it's a 64-bit mask in the K register.

I struggle to see a good way to abstract such things, even for two consecutive SIMD generations on x86.

1y agoHN ↗

A simple enough abstraction is to have the mask type have inputs of both element size and count; so for a ≤256-bit product of those you do the homogeneous-bit elements, and for 512-bit you do the packed-bit values. Conversion methods on the mask can convert those to either an explicit full vector, or a packed bit integer, as you need. (and clang can optimize out unnecessary conversions between the two)

1y agoHN ↗

Sure, that would make sense, but in AVX-512, there is also a comparison variant for 2x256-bit inputs that outputs a 32-bit mask and another one for 2x128 inputs and a 16-bit output mask. I would use those in different ways, occasionally mixing with the old AVX2 variant, depending on what I’m doing. I have tried to create a generalizable SIMD framework several times in the past, mostly in 2015-17, and still don’t have a good abstraction even between AVX2 and AVX-512.

1y agoHN ↗

You'd just produce the ≤AVX2-style result; clang will switch to using the mask-returning comparison instrs if the usage ends up being a mask (gcc doesn't do such fancy things, so I guess getting the last bits of speedup from this strategy depends on how much control over compiler selection you choose to have).

And if you really care about doing different things for ≤AVX2 vs AVX-512 masks, you're definitely in the "squeeze out the last 1.4x" camp and not the "simple ~8x speedup" one. And, realistically, you won't care about this on every single comparison, so where you really want to explicitly use mask-returning comparisons you could just switch to using intrinsics directly (or a different abstraction over them) temporarily.

As a side-note, mask-returning comparisons have 2x less throughput than homogeneous-bit-returning ones for xmm/ymm as far as uops.info data goes[1] (and use port 5!), so this strategy is kinda just what you want really.

(another strategy can be to just return the arch-specific result type, still preferring the ≤AVX2 style (with, if desired, a separate set of comparison ops for the AVX-512-mask output), and making mask-consuming ops polymorphic over the two)

[1]: https://uops.info/table.html?search=cmpeqb%20ymm%29&cb_lat=o...

1y agoHN ↗

Yes indeed, this is what we do :) There is an opaque Mask type for which operations such as CountTrue, AllFalse etc are provided. If you really want the one or other representation, VecFromMask and BitsFromMask/StoreMaskBits convert as required. The former is a no-op on AVX2.

As to the AVX2 comparison, for ICL I think we can do two of those per cycle, so no more than the throughput of an AVX-512 mask comparison. The question is what we do with it afterwards - for VQSort (one of the few applications where comparisons are really the bottleneck?) we are much happier to have the packed bits, because that can feed into vpcompress or a LUT implementing that.

1y agoHN ↗

You started with this... take:

Rust feels like a Python developer’s idea of a high-performance computing language. It’s a great language for many kinds of applications — just not when you need to squeeze out every bit of performance from advanced hardware.

And went on to say that Rust in particular is problematic for:

byte-level processing

It's particularly odd for you to say this given that the memchr Rust crate is just as fast as stringzilla for substring search. And is generally faster in cases where the needle is invariant, because stringzilla doesn't have APIs for amortizing searcher construction.

We've had a discussion about this before where I provided receipts[1] and we have not had a meeting of the minds on this point. The thing I'm trying to achieve here is to point out that your claims are contested and there is evidence that you're wrong. And so I'd caution readers to also in turn question your higher level claims about Rust being a "Python developer's idea of a high-performance computing language."

[1]: https://old.reddit.com/r/rust/comments/1ayngf6/memchr_vs_str...

1y agoHN ↗

Hey! I just mean that there is a very large category of developers, generally coming from the Python world, expecting that switching to Rust is supposed to solve every performance-oriented problem, providing a State-of-the-Art solution magically :)

Sadly, programming doesn't seem to work that way. There are always tradeoffs. Rust does some things very well, but it wouldn't be my first choice for others.

MemChr is a lovely package, and there are a few other really cool SIMD projects in the Rust ecosystem. Regardless, my development velocity for HPC-related projects is higher in C/C++, and I get much more flexibility to leverage newer hardware features, like AMX and SME.

1y agoHN ↗

Don't worry his comment is obvious clickbait (complete with a plug for a random blogpost written by him). People that matter (people that are actually writing simd for their day job) can immediately spot a poseur (especially relative to you and Raph).

1y agoHN ↗

To be clear, I don't support this. StringZilla is a real and useful project, and its performance is competitive with memchr. They aren't a poser.

(There are reasons to use StringZilla over memchr beyond performance. StringZilla provides a number of interesting string operations beyond just substring search. The memchr crate is far more specialized.)

1y agoHN ↗

Thanks, Andrew! Love your work too - keeps me on my toes ;) PS: Hoping to share a few more Rusty bits later this year.

1y agoHN ↗

Do agree that a standard SIMD type is rather pointless, if not immediately, then in like 5 years. (and, seemingly, both Rust and C++ are like over 10 years behind on SIMD, so they're already out-of-date)

What language would you consider to have cutting-edge SIMD support?

I've dipped my toes into SIMD with Rust, [1] on stable with platform-specific intrinsics (SSE2, AVX2, NEON). I would have liked to use stable `std::simd`. I learned that (particularly on AVX2) getting things into the right lanes efficiently is a pain. I would have liked to just use `simd_swizzle!` for that part, and mix that with intrinsics calls. My approach of writing a small C++ or unstable Rust program that does the swizzling and then copying the intrinsics operations it chose into my program's "source" code worked, but I prefer to not have a manual copy'n'paste step between compilation and assembly.

If there's something much better out there in another language, well, I'd be very interested to see it.

[1] I wrote this: https://github.com/infiniteathlete/pixelfmt/blob/main/docs/s...

1y agoHN ↗

Don't think any language has standardized SIMD that's particularly nice; Highway is probably a quite nice library on C++, though I haven't used it enough to get comfortable.

The thing I use for my projects is Singeli[1], a DSL specifically made for SIMD stuff (though it's capable of generally sanely doing abstractions over types/operations/loops; it's just a fancy code generator). Obligatory disclaimer that I'm one of the two people working on its design. It's far from a nice experience starting from nothing, but it's pretty nice for what I do.

CBQN's the main place it's used, can click around its source: https://github.com/dzaima/CBQN/tree/develop/src/singeli/src

Its goal isn't necessarily to unify architectures, but rather make it as easy as possible to make abstractions that do; as such its built-in includes for x86 don't have arbitrary shuffling, but do provide a sane interface over the cases that are supported in a single instruction (not including constant creation/loading), and those can run on NEON unchanged (assuming they're ran on 128-bit vectors, of course, as NEON doesn't support larger ones); and, with Singeli just generating C/C++ currently, you can just map in __builtin_shufflevector if desired. e.g. here's your AVX2 `pre`:

    include 'skin/c' # defines infix a+b & a*b etc to run __add/__mul/... (yes, those aren't here by default, and you can define custom infix/prefix ops)
    include 'arch/c' # defines __add & __mul to do C ops
    include 'arch/iintrinsic/basic' # not necessary for a shuffle, but provides basic x86 arith ops
    include 'arch/iintrinsic/select' # x86 shuffles; there's similar 'arch/neon_intrin/basic' & 'arch/neon_intrin/select' for NEON

    fn pre(inp: [32]i8, out: *[32]i8) : void = {
      store{out, 0, vec_shuffle{16, inp, merge{ # 16 specifies to repeat per 16-elt lane
        range{8}*2+1,            # lower half: 8 Y components; compile-time index calculations
        range{4}*4, range{4}*4+2 # upper half: (4 * U), (4 * V).
      }}}
    }

As a more fancy thing, I've got this working (via bodging together the definitions in CBQN with some sugar to make this pretty; not including all that boilerplate here), compilable to SSE2/AVX2/NEON producing a 4x unrolled core loop, plus tail handling (via reading past the end and doing a load-blend-store if necessary because that's what CBQN's fine with; could easily define a fancy_loop such that it does a scalar tail though). (also can be compiled to RVV via currently-unpublished mappings; no need to unroll for RVV; can choose to do either a stripmined loop or one with a separate tail):

    fn sigmoid{E}(r:*E, x:*E, n:ux) : void = {
      def V = arch_preferred_vector{E}
      @fancy_loop{V,4}(r in tup{'dst',r}, x, M in 'mask' over n) {
        # this loop body is generated 3 times for x86 & ARM - with x being a 4-elt tuple (core unrolled loop); a 1-elt tuple and no masking; a 1-elt tuple and masking
        if (any_hom{M, ...(x!=x)}) {
          emit{void, 'abort'}
        }
        r{x / __sqrt{1 + x*x}}
      }
      # were it not for a bug in tuple loop var mutation in Singeli having undesired pervasion, this would be possible:
      # @fancy_loop{V,4}(r, x, M in 'mask' over n) {
      #   if (...) ...
      #   r = x / __sqrt{1 + x*x}
      # }
    }
    export{'sigmoid', sigmoid{f32}}

(e.g. generated C for AVX2: https://godbolt.org/z/KTeGsazKP)

(I'm not actually particularly expecting interest in Singeli; I just like writing stuff :) )

[1]: https://github.com/mlochbaum/Singeli

1y agoHN ↗

Thanks for posting this, I'll take a look. It wasn't on my radar, but the idea of doing a DSL specifically for SIMD is something I've been thinking about and also starting to explore myself.

1y agoHN ↗

Thank you!

Highway is probably a quite nice library on C++, though I haven't used it enough to get comfortable.

Yeah, I've seen several references to it, including this article, so it's on my list to check out some time.

The thing I use for my projects is Singeli[1], a DSL specifically made for SIMD stuff

That is...not what I'd imagined you'd say! But I am intrigued.

1y agoHN ↗

Rust feels like a Python developer’s idea of a high-performance computing language.

I might be wrong, but I think it sounds more like Rust doesn't move as far away from the C or C++ way of doing things as you want it to. At the very least, Rust is no worse than C or C++ at any of the things you mentioned.

1y agoHN ↗

In my (biased) experience, Rust is much harder to use for advanced projects, than C and C++. On the bright side, it’s also harder to misuse :)

1y agoHN ↗

I don’t think Rust is worse at SIMD code, though.

1y agoHN ↗

I've found it much easier to use from writing a web service to writing a high performance DB that outperforms RocksDB and clearly people are using it for things like writing operating systems as well as game engines. I'm not sure what in your mind falls under "advanced projects" but I suspect it's something like number crunching (although you link StringZilla so not sure).

I'm still not seeing any description of specific challenges you feel are harder in Rust than in C/C++. In my mind Rust is completely equivalent in being able to accomplish the same tasks.

1y agoHN ↗

They wrote 2 whole paragraphs about SIMD.

1y agoHN ↗

And to me SIMD does not seem significantly harder in Rust than in C++ but I've only done a little Rust SIMD so I'm willing to be wrong on that (although others have said it's not much harder than in C++ so not sure).

1y agoHN ↗

easier to use from writing a web service to writing a high performance DB

Have you looked at what the actual implementation code of tokio / axum etc looks like?

I can't comment on DB because that is quite far out of my wheelhouse but regarding game engines, I've almost universally seen people revert to storing objects in arrays and passing around "handles" which is literally pointers with extra steps... But I guess at least you are protected from some issues there.

Rust forces absurd levels of abstraction onto code very early and if you are wrong you need to make sweeping refractors.

I really want to love the language, but building bottom up is extremely difficult with it, it may be Stockholm syndrome and familiarity but I can generally get much further into a project much quicker in C++.

In Rust it feels like I'm fighting the language and it's not the borrow checker.

There are so many great ideas in Rust and I keep trying it because of them, don't get me wrong I do think a safer language is the future but Rust isn't there yet. Every time I try it it's better though.

1y agoHN ↗

I have actually looked at the interior of Tokio. While there's a lot of complexity there, most of that complexity is in making a highly performing multi-threaded work-stealing runtime rather than anything Rust-specific. Indeed, I find it easier to reason about than in C++ because the ownership rules are enforced and where they're violated is clearly annotated with unsafe & lots of documentation explaining why it's safe. I suspect you'll see similar things in axum but I can't speak to that codebase specifically.

I don't think you'd be able to do something meaningfully easier in C+. I think it is Stockholm syndrome. It took me about 2 months to get familiar that it wasn't uncomfortable anymore and 8 months to reach the "I can generate code faster than I ever could in C++".

For context consider that I'd spent the prior ~11 years coding on and off in C++ professionally and the prior 10 years before that as a kid learning C++ so C++ was definitely a language I felt at home in. I spend almost no time compared to C++ trying to make the build system work and I can quickly pull in high quality components others have written vs in C++ where I either have to implement it myself, spend time on build integration, or figure out a way to do without.

1y agoHN ↗

Oh I completely agree Tokio is a complex piece of software, but my point was you need to implement a decent amount of that even for trivial cases where Tokio isn't needed and therefore the default response is to just pull in Tokio.

Part of the complexity for me at least in async Rust was that it is some weird mix between coroutines and completion handlers. It's a pretty nice abstraction with once you wrap your head around it but how many Rust devs would be able to do that?

Should I find a performance regression and need to dive in to the code it takes a lot of unknown unknown to be explored because what is actually happening is hidden from you by the abstraction and the Async book's response is "don't worry about that just use tokio". And the "advanced" section is incomplete.

I've almost always found the complexity of these systems to be reasonable when I've dug through it, but I had to manually mess with Rust code to understand the async model, the book failed to explain what it is and when it clicked it made a ton of sense.

Maybe my issue is that Rust just forces abstractions down your throat and says "trust me you need it" but I would much rather discover why and even find a more relevant abstraction.

The last example I made still stands, there is so much complexity caused by abstractions that the wgpu "book" recommends an older version of winit because the current version made API changes that requires a lot of code change. Winit 0.30 released almost a year ago, I understand that it is likely a volunteer maintainer there but if the language allowed for easy refactoring why has it been a year?

The fact that the webgpu standard is still in active development likely adds to that.

1y agoHN ↗

Commenting on two loosely-coupled things here!

The WGPU/Winit compatibility situation is indeed a mess. (Throw EGUI in there too if you're into that...) I was able to get all of them onto the latest, but it was an adventure of troubleshooting, and asking for help on Discord, Github etc. I think this could have been avoided by adding current and/or practical examples to the WGPU repo and/or docs. So, documentation problem maybe? Most of the examples are too provincial or trivial to use as a basis for building a practical project. (Or upgrading one!)

Count me as someone who can't grok Async. I've tried several times. It doesn't stick to my brain, and/or I'm not sure how to keep it from propagating through the code. I've come to the realization that I will probably never get it, and/or see eye-to-eye with the rust community on this. Most of the web ecosystem will remain off-limits.

1y agoHN ↗

I can't comment on DB because that is quite far out of my wheelhouse but regarding game engines, I've almost universally seen people revert to storing objects in arrays and passing around "handles" which is literally pointers with extra steps... But I guess at least you are protected from some issues there.

Regarding game engines, the empirical evidence shows that Bevy has been moving at least as fast as, if not faster than†, comparable C++ engines. I listed all the features I landed in the past year here: https://news.ycombinator.com/item?id=42945730

We love to argue on message boards about theoretical or anecdotal productivity of programming languages, but at the end of the day the only thing that matters is what people have actually been doing with the language, and for Rust the answer is "quite a lot, actually".

†In my view, Bevy has been moving significantly faster than comparable C++ engines, but I don't need to argue that to make my point.

1y agoHN ↗

Don't take it as an attack on game engines in Rust. It really isn't, it's more an observation that people happily work around the fundamental language design and that should be an indicator to the language authors.

And they are definitely listening though, when I last took a dive into Rust I faught a lot of weird borrow checker edge cases that I could easily verify as correct. The borrow checker now no longer complains about some of those cases.

As I said I think it's a language maturity thing. It will get there.

1y agoHN ↗

I'm still not seeing any description of specific challenges you feel are harder in Rust than in C/C++

It's harder to model domains in Rust. C has the same problem and it is a big reason why the industry standard for game dev is C++

1y agoHN ↗

Could you expand on this? What exactly doe "model domains" mean?

1y agoHN ↗

Care to give an example? I think GUIs remain the softest domain modeling area, but that’s about how do you do next gen GUI toolkits that are super high performance, safe, and lower overhead vs what Rust has today. But that’s an underserved niche anyway vs c++ toolkits or electron.

The other weak parts might be ecosystem immaturity (eg Unreal vs Bevy) but that’s not a language modelling issue.

1y agoHN ↗

Rust makes it hard to represent mutable graphs. If I have a value that can be updated by multiple GUI nodes, I have to architect my way around the borrow checker in Rust. Is it safe? yes. Is is performant? for a skilled Rust dev, yes. But it takes me >25% more time to do compared to C++. 25% of a year long project is 3 months

1y agoHN ↗

In my experience Rust is much easier for advanced projects. Once you get into high performance code, C++ takes much more time, for two reasons.

1. With C++ you have to think a lot. Like, a lot. To convince yourself that what you are doing is safe, and that the lifetimes and races of various objects are safe. Rust takes a huge load off, by failing to compile your mistakes. You know that for the code that doesn't use "unsafe", if it compiled, then you don't even have to think about races or lifetimes.

2. C++ has so many places where copies sneak in. Tracking down needless copies in C++, for object types where it's not as simple as "just disable copy construct & copy assign", can be tricky and is extremely brittle to future changes of code. And not just for the CPU cost of copies, but RAM costs too.

And I say that as someone who's been coding C++ on a daily basis since the 1990s.

1y agoHN ↗

Rust is easy to understand as "a language by browser writers for writing browsers." That statement alone gives you most of Rust's design choices:

* Safety over everything else

* Very good single threaded performance

* Javascript-like syntax and a Javascript-like package manager

I have been working on some software in Rust recently that needs bit and byte manipulation, and we have "unsafe" everywhere and hugely complicated spaghetti compared to the equivalent code in C.

1y agoHN ↗

Most likely because many keep forgetting that bit and byte manipulation in C is a mix of implementation defined and UB, depending on how it is coded.

1y agoHN ↗

What's the problem of using toolchain-specific features instead of behavior defined by the standard? Isn't this the bread and butter of embedded development and the reason why some behavior is purposely left undefined in the standard?

1y agoHN ↗

[...] and the reason why some behavior is purposely left undefined in the standard?

It's one of the saner reasons why there's UB in C and C++. But there's lots of crazier reasons.

1y agoHN ↗

It's one of the saner reasons why there's UB in C and C++.

It is the reason why the standard purposely leaves some areas undefined.

C and C++ detractors talk a lot about UB but they always show their understanding on the subject is at best superficial. The parrot UB as if it was this major gotcha, when it is literally behavior the standardization committees make a call to not define behavior to purposely leave it open, so that any implementation can still be conforming even if they decide to implement behavior not defined in the standard. It is that simple, but somehow people parrot UB behavior as if it was this major gotcha. Baffling.

1y agoHN ↗

Baffling is the ignorance in the ways of WG14 and WG21, while pretending to be a know it all, maybe update yourself on the relevant papers for C2y and C++26, aimed at clearing up references to UB with erranous behaviour, or implementation defined.

1y agoHN ↗

And then it was a decision on the part of compiler vendors to define it to do insane things like time travel. Not the standards committee. Compiler vendors could have just as well defined it to wrap.

1y agoHN ↗

Time travel is a consequence of UB anywhere in an execution making the whole execution UB.

The committee could have said that UB only makes the rest of the execution UB, not what happened before.

1y agoHN ↗

Exactly. The classic example is invalid pointer deref. It’s too costly to check every deref against every allocation (outside of special debug modes). So the system usually can only detect if there is a virtual memory page fault, in which case it can crash.

In embedded systems without virtual memory there is no validity checking at all. Or it could periodically check (random sampling).

So the standard makes the reasonable choice to leave it undefined. If you can detect it and crash, that’s great. If not and you accidentally overwrite your programs instructions, anything can happen.

Rust only avoids UB in so far as it relies on default clang behavior on modern hardware.

1y agoHN ↗

You’re describing implementation-defined behavior, not undefined behavior. IB does something but the committee doesn’t say what. UB does anything and the compiler can assume you never intended to cause it.

1y agoHN ↗

It is the reason why the standard purposely leaves some areas undefined.

No. It is _one_ of the reasons, not _the_ reason.

For example, signed integer overflow. If all you were worried about was the behaviour of the underlying hardware, you would declare this _implementation defined_. (With the vast majority of implementations today picking twos-complement.)

Instead, even compilers that target two-complement machines leave this deliberately undefined, so they can target exploit this corner for performance gains, without actually have to properly prove that leaving out certain checks would be sound under twos-complement.

1y agoHN ↗

Security exploits is the problem, because what common people that don't read WG14 mailings think the word undefined means, and what everyone else involved with creating compilers understand what they are allowed to do, is not the same.

1y agoHN ↗

Security exploits is the problem, because what common people that don't read WG14 mailings think the word undefined means, and what everyone else involved with creating compilers understand what they are allowed to do, is not the same.

Your comment lacks credibility. Your hypothetical scenario would only be conceivable if a) a team was well versed enough in C to adopt a specific toolchain to leverage implementetion-defined behavior that leveraged behavior left undefined by the C standard, b) somehow the same team decides on a whim to replace their toolset with some other random toolset without eve being aware of their toolset-specific code. This is far from a realistic scenario, and reads more like mindless complains about UB coming from a place of ignorance.

1y agoHN ↗

CVE database, the ongoing liability laws in cybersecurity across several nations, and companies that also happen to be C and C++ compiler vendors, are my credibility.

It is incredible how for the last 50 years we keep getting ad-hominens from folks that think actually to know anything at all about security, and only clueless junior developers don't know what they are doing.

1y agoHN ↗

Exactly this. Most of the art of it is avoiding UB and sticking to implementation-defined behavior.

1y agoHN ↗

I have been working on some software in Rust recently that needs bit and byte manipulation, and we have "unsafe" everywhere and hugely complicated spaghetti compared to the equivalent code in C.

I'm curious what makes this so different from my experience. I rarely ever have to write "unsafe", and I'm writing quite low-level engine code that certainly uses bit manipulation. In fact, crates like bitflags and fixedbitset make it so easy that I tend to get dinged in code reviews for using bit flags when structs of booleans would be simpler :)

1y agoHN ↗

I'm curious what makes this so different from my experience. I rarely ever have to write "unsafe", and I'm writing quite low-level engine code that certainly uses bit manipulation.

Perhaps your usecase is similar enough to eg JavaScript engines? Because that's a usecase that browser writers would at least have in mind?

1y agoHN ↗

It's not. It's Bevy.

What effects this is separating your unsafe into unsafe abstractions. If you're careful you don't have to write too much unsafe. Granted it's not easy.

1y agoHN ↗

A game engine is much more similar to a browser engine than you think. The operations you have discussed, using bits as flags, are also the most basic forms of binary-level manipulation out there. Things like tagged pointers and bit flags fit nicely and neatly into an encapsulated unsafe abstraction (provided you want to add an extra 1000 lines of code for it).

As far as I can tell, any time you want to rely on the exact binary layout of something in memory, you need unsafe. As a corollary, any time you want to bit cast from one type to another, you need unsafe. This means that things like succinct data structures and building network protocols need quite a bit of unsafe everywhere. The former needs you to do things like "store 14 bits of X here and 12 bits there..." The latter needs control of bit and byte layout because you want to carefully eliminate implementation-defined compiler behavior.

1y agoHN ↗

Take a look into bytemuck or zerocopy. I haven't used unsafe when doing byte level manipulation in a long time.

1y agoHN ↗

And a jet engine and IC engine are engines, but putting your car engine into a Boeing and vice versa would be an unwise decision.

I'd argue you're over-abstracting the differences. They have different purposes. Game engines need high performance, while browsers need to enable wide selection of APIs.

1y agoHN ↗

And a jet engine and IC engine are engines, but putting your car engine into a Boeing and vice versa would be an unwise decision.

Sure, but you'd expect the design software for a car engine to be at least half-way relevant for a jet engine in a pinch.

Compared to eg the software an architect would use to design a bridge.

1y agoHN ↗

As far as I can tell, any time you want to rely on the exact binary layout of something in memory, you need unsafe. As a corollary, any time you want to bit cast from one type to another, you need unsafe.

No, that's what bytemuck is for. If bytemuck didn't exist, sure, I'd be using a lot of unsafe.

1y agoHN ↗

* Javascript-like syntax and a Javascript-like package manager

I think this is not serious criticism. The "javascript-like package manager" reference actually refers to fixing a major problem with the developer experience playing legacy programming languages such as C or C++. Java has those, .NET has those, every single mainstream programming language has those. Except C or C++.

Rust might be riddled with "the emperor has no clothes" aspects, but having a package manager is not it.

1y agoHN ↗

That's not a negative aspect of Rust, and I'm not picking out a list of things I dislike about Rust. Making a statement that isn't unequivocally positive does not equal criticizing something.

Cargo is, however, a similarity with JS. On the whole a good one. Also, cargo works much more like npm than maven, for example.

1y agoHN ↗

Cargo has been co-created by Yehuda Katz, who worked on Ruby's Bundler before. Cargo has been designed after npm, so it definitely took lessons from it, but it doesn't make sense to just broadly attribute this to Rust being JavaScript-adjacent.

The Rust syntax is not coming from JavaScript. It even has conflict with it, using `let` and `const` differently, since the `let` in Rust comes from Ocaml, not JS.

Both JS and Rust copy from the same C/C++ roots. Rust's curly brace flavor is more similar to Go and Swift. The original author of Rust liked a lot of languages with different syntaxes, but the C-like syntax has been a pragmatic choice to avoid putting off the target audience of C++ programmers:

http://venge.net/graydon/talks/intro-talk-2.pdf

1y agoHN ↗

I've written a fair bit of SIMD code in Rust, and it definitely had lots of sore spots.

The main advantage was that, because Rust doesn't use TBAA, it's completely legal (and safe, if you use bytemuck) to freely cast pointers and values around. TBAA in C++ makes it much easier to hit undefined behavior.

But also, because of various miscompilations, Rust refuses (or at least refused) to pass SIMD arguments in registers, so every non-inlined function call passes arguments via the stack. There were also miscompilations if you enabled a target_feature for just one function, so we ended up just passing `-C target-cpu=...` globally, and if we wanted to support a different microarchitecture, we just recompiled the whole program. On top of that, there's no good way to check to see what microarchitecture you're compiling for, so we had to resort to specifying the target cpu in multiple places, with comments reminding us to keep the places in sync.

1y agoHN ↗

Even before getting into SIMD, try using Rust for concurrent, succinct, or external-memory data structures. It quickly becomes clear where the friction is.

It's the exact opposite for me: I use concurrent data structures more often in Rust than I do in C++ because I don't have to worry about dumb data race bugs. If one of my Bevy systems is slow, I slap par_iter() on the query and if it compiles it probably works, or at least fails for a not-stupid reason.

1y agoHN ↗

What do you think about 'par_iter' having to wait for work imbalance to return execution back? With 'when_all' like primitive one can continue execution on any thread without losing one for waiting. ps as someone who does not have rust job i would like to see an example how rust deals with task based systems if you have public one at hand ofc.

1y agoHN ↗

The rayon library uses work stealing for this. Its parallel iterators offer some control of splitting strategies and granularity, so you can tune the trade-off between full utilization and cost of moving things between threads.

Additionally, in Bevy, independent queries (systems) are executed in parallel, so there's always something else to do, except your one worst loop.

1y agoHN ↗

Concurrent data structures are rarely faster than "lock + non-concurrent data structure" and if you're putting constructs like par_iter() in a lot of places, there's a good chance you would be better off with the "dumb" pattern than the concurrent data structure.

The same goes with Arc - if you're using it a lot there's a good chance that code with a lot of Arcs is slower than equivalent GC-ed code.

1y agoHN ↗

While it's true that par_iter() uses a concurrent data structure under the hood, it's specifically designed to use work-stealing to avoid needing threads to communicate.

Why would putting a lock over a global workqueue be faster than per-thread workqueues that don't require inter-thread communication (except in the case where work-stealing is required)?

1y agoHN ↗

Atomics are very expensive operations. Lock/unlock is two atomics. Many concurrent data structures will end up doing many more atomic operations than you expect.

The general wins of concurrent data structures come when you really are accessing them truly concurrently - as in when many threads on many cores are heavily contending for access and you need to make global progress.

1y agoHN ↗

Sure! However, the work-stealing queue in rayon [1] uses three atomic operations instead of the two atomic operations for a mutex for a global lock. The difference, however, is the three atomic operations for the thread-local queue should be uncontended, whereas a global lock on a global work queue would experience contention from every thread trying to access it for jobs.

Between the choices of "single work sharing queue with a big mutex on it that all threads access for work" vs "per-thread work-stealing queue that's uncontended for the cost of one extra atomic," in what situations would the work-sharing queue with the global mutex outperform? Perhaps if there's a small number of jobs, and there's not enough time for the work-stealing algorithm to distribute jobs to the worker threads before the work-sharing algorithm has already finished.

[1]: https://github.com/crossbeam-rs/crossbeam/blob/423e46fe20471...

1y agoHN ↗

Run a benchmark. With low contention, the lock will outperform. Atomics are very expensive assembly instructions.

Fedor Pikus has a good talk on this at cppcon 2019.

1y agoHN ↗

Atomics are very expensive operations.

Atomics are very expensive when contended - which is also the case where locks would introduce blocking and reduced performance. Uncontended atomics are relatively cheap.

1y agoHN ↗

I suggest you run a microbenchmark. Uncontended atomics are some of the most expensive assembly instructions out there. They also acquire global system locks on certain stages of execution.

1y agoHN ↗

Yep, including draining the store buffer.

We've gotten our ThreadPool barrier+wait to use only acq/rel, but not yet the work stealing. Does anyone have experience with that already?

1y agoHN ↗

FWIW, work stealing can be pretty expensive. It is only efficient under a narrow set of assumptions about the workload.

1y agoHN ↗

One of the things I find interesting about SIMD is that a lot of behavior that is “undefined” for scalar types in C-derived languages is explicitly fully defined when you use SIMD intrinsics with the same integral types. UB exists to hide the fact that major CPU architectures give different results for basic ALU operations in some cases. SIMD makes no such pretense of abstraction. If I am using AVX-512 I explicitly get the full Intel architecture experience, the implementation details are not hidden behind UB. Same with ARM, etc.

For example, shift overflows are masked on x86, zero-filled on ARM, and undefined in C/C++. In SIMD-land, none of this is hidden and so you design your code to leverage the reality that those instructions behave differently, whereas in C/C++ only the behavior they have in common is “defined”.

The vector ISAs are sufficiently different from each other (and normal CPUs) that it is like trying to build a compiler that can automagically produce optimized code for both CPUs and GPUs from the same source tree. I am not optimistic that this will happen anytime soon. AVX-512 essentially started life as a GPU ISA, which probably explains the interesting fact that a modern x86 CPU core has more AVX-512 registers than x86 registers.

1y agoHN ↗

Of course. Many things are UB because dictating a policy for all machines doesn’t make sense. Whereas AVX is a specification for a specific hardware capability.

1y agoHN ↗

Python doesn't have anything remotely similar to Cargo that isn't written in Rust (uv is). It's exactly backwards. Python is benefitting from Rust philosophy.

1y agoHN ↗

Rust feels like a Python developer’s idea of a high-performance computing language.

I'd say the language complexity (and it keeps piling more things) means it's pretty far from Python's ideas.

Julia is closer.

1y agoHN ↗

Rust is an extraordinary simple language. Compare it to the complexity of C++.

1y agoHN ↗

We can say that Mars is more similar to Earth than Jupiter even though Sagittarius A* exists.

1y agoHN ↗

Unsure which celestial body is supposed to be which in this comparison :p

1y agoHN ↗

If you only compare against C++, maybe. Otherwise, no. C++ and Rust are in a class of their own when it comes to compile times and complexity.

1y agoHN ↗

Well C++ is the only major language (other than C itself) that occupies the same space. So it's natural that they are compared. And Rust is an order of magnitude simpler language than C++ almost comical web of intricacy.

1y agoHN ↗

I’m seriously interested; what is the best language/franework/method that will squeeze out every bit of performance from advanced hardware that you know of?

1y agoHN ↗

I’ve been playing around with SIMD since uni lectures about 10 years ago. Back then I started with OpenMP, then moved to x86 intrinsics with AVX. Lately I’ve been exploring portable SIMD for a side project where I’m (re)writing a Numpy-like library in Rust, mostly sticking to the standard library. Portable SIMD has been super helpful so far.

I’m on an M-series MacBook now but still want to target x86 as well, and without portable SIMD that would’ve been a headache.

If anyone’s curious, the project is here: https://github.com/IgorSusmelj/rustynum. It's just a learning exercise for learning Rust, but I’m having a lot of fun with it.

1y agoHN ↗

A problem for RISC-V is going to be that there's currently no way for user code to detect the presence of RVV. I have no idea how you can do multiversioning with that limitation.

1y agoHN ↗

The solution is to ask the OS to detect it for you. Linux offers a syscall for this (riscv_hwprobe). Has the drawback that it requires OS support, of course. But RVV requires OS support anyway (e.g. managing mstatus, saving vector registers on context switch), so that seems fine to me.

1y agoHN ↗

Oh? Isn't that what this does? std::arch::is_riscv_feature_detected!("v")

Hmm… now that I actually experiment with it, I can't get it to return `true` on hardware that does support it, unless I also compile with -Ctarget-feature=+v. And if I do, then the binary crashes with SIGILL before getting to that point on hardware without rvv.

So if it's always equal to cfg!(target_feature="v"), then what does that even mean?

I have created https://github.com/rust-lang/rust/issues/139139

1y agoHN ↗

What happens there when you try to execute a missing RVV instruction ? On other archs you get a SIGILL which you can handle.

1y agoHN ↗

With RISC-V being an open ISA a vendor can freely add some non-RVV thing on the instruction space used by RVV if it doesn't desire to support RVV. And that already exists with Xtheadvector (aka RVV 0.7.1) where thead made hardware with pre-ratification RVV that's rather incompatible with the ratified RVV1.0 but still uses generally the same encoding space.

1y agoHN ↗

It's "reserved" which is basically the same as C's UB - anything can happen (nasal dragons etc.) so you can't rely on it.

1y agoHN ↗

Just because it's reserved doesn't mean anything can happen. On x86_64, you get a clearly defined error when you use reserved bits and the like. I don't know if RISC-V is the same, but if it isn't it's because they chose to be vague, not because that's what it means to be reserved.

1y agoHN ↗

For what it's worth, the RISC-V unprivileged spec says:

The behavior upon decoding a reserved instruction is UNSPECIFIED.

Some platforms may require that opcodes reserved for standard use raise an illegal-instruction exception. Other platforms may permit reserved opcode space be used for non-conforming extensions.

The RVA23 says: (https://github.com/riscv/riscv-profiles/blob/main/src/rva23-...)

Implementations are strongly recommended to raise illegal-instruction exceptions when attempting to execute unimplemented opcodes or access unimplemented CSRs.

and has an optional extension "Ssstrict":

Ssstrict No non-conforming extensions are present. Attempts to execute unimplemented opcodes or access unimplemented CSRs in the standard or reserved encoding spaces raises an illegal instruction exception that results in a contained trap to the supervisor-mode trap handler.

1y agoHN ↗

I guess being unpriviliged this still guarantees the dragons are restricted your process, otherwise the chip would have a security problem. So as a heuristic you could fork your process, and try to execute SIMD operations (detecting known nonstandard versions and failing them), and if results seems fine send a "ok" flag up a pipe. (or compile this into a separate test executable)

1y agoHN ↗

It's a shame that the `MISA` CSR is in the 'Privileged Architecture' spec, otherwise you could just check bit 21 for 'V', but that appears to only be available in the highest privilege machine-mode.

Presumably your OS could trap attempts to read the CSR and allow it, but if not then it's a fatal error and your program shits the bed, otherwise you rely on some OS-specific way of getting that info at runtime.