[HN Gopher] Show HN: Rust Vector and Quaternion Lib
       ___________________________________________________________________
        
       Show HN: Rust Vector and Quaternion Lib
        
       I use this library I made for Vectors and Quaternions in many
       personal projects. I've open-sourced it, in case anyone else would
       get use out of it.  I use this on various projects, including
       quadcopter firmware, a graphics engine, a cosmology simulation, and
       several molecular dynamics applications. No_std compatible.
        
       Author : the__alchemist
       Score  : 121 points
       Date   : 2025-03-06 20:32 UTC (1 days ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | tux3 wrote:
       | Sounds interesting, but it's a space with a lot of different
       | options. Have tried doing benchmarks against other vector
       | libraries?
       | 
       | If I'm shopping for a vector library, this is one of the pieces
       | of information that makes the decision easier.
        
         | mflaherty22 wrote:
         | I'd like to see what the author's motivation was for making
         | this, but I'm not sure benchmarks would be the first thing I
         | look for :)
        
           | the__alchemist wrote:
           | I haven't done benchmarks, but this speculation nailed it -
           | The exact motivation is lost, but I believe I became tilted
           | with inconsistent mixes of JPL and Hamilton quaternion
           | operations on the popular libs at the time. Since then, I use
           | it as a low-friction way to conserve geometrical functions
           | among multiple projects. I.e., it's easier to just edit the
           | code base than go through a PR process.
        
             | zamalek wrote:
             | no_std seems somewhat novel, which might have been another
             | motivating factor?
        
         | creata wrote:
         | I haven't benchmarked, so these opinions might be worthless,
         | but here's how they're laid out internally.
         | 
         | * nalgebra uses fixed-size arrays (so a Vec4 is like [[f32; 4];
         | 1])
         | 
         | * this library seems to use fields (so a Vec4 is a struct with
         | x,y,z,w fields)
         | 
         | * glam uses SIMD types for some types (so a Vec4 is a __m128)
         | 
         | I think maybe glam _might_ win for some operations, but if you
         | want performance, people usually SIMD in the other direction
         | when possible, like:                   struct Vec4 { x: __m128,
         | y: __m128, z: __m128, w: __m128 }
         | 
         | According to mathbench-rs[0] (which I looked at after typing
         | this comment...) it looks like nalgebra and ultraviolet have
         | such types. The benchmarks have "N/A" for many of the "wide"
         | nalgebra entries though, which _might_ indicate that nalgebra
         | hasn 't implemented many of those functions for "wide" types.
         | 
         | [0]: https://github.com/bitshifter/mathbench-rs
        
           | Animats wrote:
           | The glam guy wanted to go all aligned so that SIMD would
           | work, but that would break so much code that he was talked
           | out of it.
           | 
           | Hint for language designers: when you design a new language,
           | put this stuff, and multidimensional arrays, in the standard
           | library. Multiple incompatible versions of such types is as
           | bad for number-crunching as would be multiple incompatible
           | string types for string manipulation. You want your standard
           | numeric libraries to work on the standard types.
           | 
           | This is part of why Matlab is so successful. You don't have
           | to worry about this stuff.
        
             | zevets wrote:
             | It's honestly surprising so many programming languages
             | ignore the needs of "floating point" users. Rust has ints
             | that aren't 0, but no std type for floats that aren't NaN?
             | In some sense, ieee754 floats are better than ints, as the
             | float error modes have NaNs are just HW supported error
             | tagged enum types.
             | 
             | I think its from a CS education which treats the "naturals"
             | as fundamental, vs an engineering background where the
             | "reals" are fundamental, and matrix math _essential_ and
             | people live on one side of this fence.
        
               | vlovich123 wrote:
               | > This enables some memory layout optimization. For
               | example, Option<NonZero<u32>> is the same size as u32
               | 
               | NaN doesn't have this optimization because the
               | optimization isn't generic across all possible
               | representations. Trying to make it generic gets quite
               | complex and floats might have many such representations
               | (eg you want NaN to be optimized, someone else needs NaN
               | and thinks infinity works better etc). In other words:
               | 
               | Nonzero is primarily for size optimization of
               | Option<number>. If you want sentinels, then write your
               | own wrapper, it's not hard.
        
               | Animats wrote:
               | That was true in the past, for a few reasons.
               | 
               | - Floating point operations used to be slow. On early
               | PCs, you didn't even have a floating point unit. AutoCAD
               | on DOS required an FPU, and this was controversial at the
               | time.
               | 
               | - Using the FPU inside system code was a no-no for a long
               | time. Floating point usage inside the Linux kernel is
               | still strongly discouraged.[1] System programmers tended
               | not to think in terms of floating point.
               | 
               | - Attempts to put multidimensional arrays in modern
               | languages tend to result in bikeshedding. If a language
               | has array slices, some people want multidimensional
               | slices. That requires "stride" fields on slices, which
               | slows down slice indexing. Now there are two factions
               | arguing. Rust and Go both churned on this in the early
               | days, and neither came out with a good language-level
               | solution. It's embarrassing that FORTRAN has better
               | multidimensional arrays.
               | 
               | Now that the AI world, the GPU world, and the graphics
               | world all run on floating point arrays, it's time to get
               | past that.
               | 
               | [1] https://www.kernel.org/doc/html/next/core-
               | api/floating-point...
        
           | grandempire wrote:
           | The code example is absolutely the way to do simd. A simd
           | type is not a geometric vector, it's a magic float that
           | happens to do 4 float operations at a time.
           | 
           | If your vector is generic (using cpp syntax here): vec<3
           | float> then you can just put in vec<3, float4> and then solve
           | 4 vector math problems at a time.
           | 
           | It helps tremendously if your interfaces already take N
           | inputs at a time, so then instead of iterating one at a time
           | you do 4 at a time.
        
             | creata wrote:
             | Right. Glam (maybe because it's stuck with its data layout,
             | maybe to present a cleaner interface) instead uses a SIMD
             | type for a single Vec4, which tends to be a much less
             | efficient way of using SIMD types.
             | 
             | > If your vector is generic (using cpp syntax here): vec<3
             | float> then you can just put in vec<3, float4> and then
             | solve 4 vector math problems at a time.
             | 
             | Yeah, that's the idea, but for anyone reading, the main
             | complication is when you need to branch. There are usually
             | multiple ways to handle branching (e.g., sometimes it's
             | worth adding a "fast path" for when all the branches are
             | true, and sometimes it isn't; sometimes you should turn a
             | branch into branchless code and sometimes you shouldn't)
             | and AVX-512 adds even more ways to do it.
        
           | camel-cdr wrote:
           | I'm not a fan of such vector libraries, AFAIK they all just
           | inhibit auto vectorization. At most, you can take advantage
           | of 128-bit SIMD with a bunch of shuffles and extracts,
           | whenever you are also working with scalar variables.
           | 
           | I did a small experiment comparing 6 possible implementations
           | of the n-body [0] update loop:
           | https://godbolt.org/z/sfehEfPGT
           | 
           | The implementations are:
           | 
           | * AOS: a simple scalar implementation with coordinates stored
           | in an array of structs * SOA: a simple scalar implementation
           | with coordinates stored as a struct of arrays * float3: uses
           | a struct of three floats as a vector type * float4: uses a
           | struct of four floats as a vector type, ignores the last
           | element * vec4: like float4, but using a generic SIMD
           | abstraction (so basically what glam does) * floats3: attempts
           | to do SOA with nice syntax. floats3 type has three arrays of
           | floats and there are operations to extract and store a float3
           | type from a given index.
           | 
           | Since these abstractions are often used in games I'll start
           | of looking at what the compiler produces when targeting Zen5
           | with -O3 -ffast-math:
           | 
           | * Zen5 O3 ffast-math:                   AOS:     gcc: 11119
           | ~SSE    clang:  3688 AVX512, but quite messy         SOA:
           | gcc:  1283 AVX512  clang:  1202 AVX512         float3:
           | 11050 ~SSE    clang: 10894 ~SSE         float4:  gcc:  8646
           | ~SSE    clang: 10815 ~SSE         vec4:    gcc:  7913 ~SSE
           | clang:  8196 ~SSE         floats3: gcc:  1284 AVX512  clang:
           | 13351 ~SSE
           | 
           | The numbers next to the compilers are the cycle estimates
           | from the llvm-mca model of Zen5 for processing 1024 elements.
           | AVX512 indicates whether the compiler was able to vectorize
           | the loop with AVX512, and ~SSE means it could be partial
           | vectorization with SSE.
           | 
           | Now let's also look at a different ISA, this time the RISC-V
           | Vector extension:
           | 
           | * P670 2xVLEN O3 ffast-math:                   AOS:     gcc:
           | 17445         clang:  3357 RVV         SOA:     gcc:  3355
           | RVV     clang:  3334 RVV         float3:  gcc: 17445
           | clang: 17449         float4:  gcc: 25668 RVV128  clang: 17470
           | RVV128         vec4:    gcc: 45091 RVV128  clang: 23111
           | RVV128         floats3: gcc:  3333 RVV     clang: 17446
           | 
           | This time the llvm-mca model for the SiFive-P670 was used,
           | but I pretended it has 256-bit vectors instead of 128-bit
           | ones, as the vector length is transparent to the codegen and
           | this amplifies the effect I'd like to show. RVV means it
           | could be fully vectorized, while RVV128 is similar to ~SSE
           | and means it could only partially take advantage of the lower
           | 128-bit of the vector registers.
           | 
           | So if you are using such vector types to do computations in
           | loops you are likely to end up preventing your compiler from
           | optimizing it for modern hardware. In general writing simple
           | SOA scalar code seems to vectorize best, as long as you make
           | sure the compiler isn't confused by aliasing. But even the
           | plain old AOS scalar code can be vectorized by modern clang,
           | but not by gcc, and sadly also not the float3/float4
           | implementations, which should be very similar. Modern ISAs
           | like NEON/SVE/RVV have more complex vector load/stores that
           | allow you to retrieve data more efficiently even from a
           | traditionally bad data layout like AOS. You can dress up the
           | SOA code to make it a bit nicer, unfortunately my attempt
           | with floats3 currently only works properly with gcc.
           | 
           | Below are the results when compiling without -ffast-math:
           | 
           | * Zen5 O3:                   AOS:     gcc: 11819 ~SSE
           | clang: 10788 ~SSE         SOA:     gcc:  4146 AVX512  clang:
           | 13734 AVX512         float3:       11826 ~SSE    clang: 11499
           | ~SSE         float4:  gcc:  8662 ~SSE    clang: 11810 ~SSE
           | vec4:    gcc:  8575 ~SSE    clang:  7451 ~SSE
           | floats3: gcc:  4148 AVX512  clang: 14367 ~SSE
           | 
           | * P670 2xVLEN O3:                   AOS:     gcc: 17464 RVV64
           | clang:  6122 RVV         SOA:     gcc:  7140 RVV     clang:
           | 6118 RVV         float3:  gcc: 17445         clang: 17464
           | RVV64         float4:  gcc: 25665 RVV128  clang: 19184 RVV128
           | vec4:    gcc: 17463 RVV128  clang: 56868 RVV128
           | floats3: gcc:  7140 RVV     clang: 17444
           | 
           | Weirdly clang seems to be struggling with the SOA here, and
           | overall vec4 looks like the best performance tradeoff for
           | X86. Still with proper SOA, and I bet you could coax clang
           | into generating it as well, you can still get a 2x
           | performance improvement. Additionally, vec4 performs horribly
           | with current compilers for VLA SIMD ISAs.
           | 
           | I'll try to experiment with some real world code, if I can
           | find some that is bottle-necked by such types.
           | 
           | [0] https://benchmarksgame-
           | team.pages.debian.net/benchmarksgame/...
        
         | exDM69 wrote:
         | I'm using Rust (nightly) std::simd for my 3d math stuff. It
         | doesn't come with matrix and quaternion functions but they were
         | relatively simple to implement based on known good algorithms.
         | But you do get basic arithmetic operators without having to
         | implement std::ops by hand (or with macros).
         | 
         | What was/is not pretty is adding some generics so the same code
         | can work with f32 and f64. I did manage to get something that
         | works but it's so ugly that I didn't want to release it. I'm
         | sure it could be improved but what I got works well enough and
         | I haven't needed to touch it in a few years.
         | 
         | std::simd is quite pleasant to work with and most importantly
         | it allows a zero cost fallback to CPU specific intrinsics when
         | needed.
        
       | simojo wrote:
       | how does it compare to nalgebra?
        
       | kvark wrote:
       | The gamedev ecosystem appears to be split between nalgebra (for
       | Rapier users) and glam. Where does lin-alg fit?
        
         | pttrn wrote:
         | cgmath?
        
           | bladeee wrote:
           | cgmath is nice, but it uses old Rust 2015 and hasn't been
           | updated since January 2021.
        
             | tialaramex wrote:
             | I can't imagine any obvious reason I would miss Rust's 2018
             | edition, let alone 2024 edition, to implement linear
             | algebra? People seemed happy enough in Fortran before I was
             | old enough to go to _school_ and I don 't sense it's an
             | application where I'd want async for example. A lot of
             | other edition changes are nice _when writing new thing_ but
             | not helpful for an existing codebase. So, like, sure, it 's
             | 2015 edition but that's fine?
        
             | foresterre wrote:
             | There's actually an advantage to using older editions, and
             | that is that it lowers the MSRV (minimum supported Rust
             | version). This is especially nice for libraries, while
             | binary project usually can just use the latest edition.
        
             | duped wrote:
             | it's updated, just not on crates.io
        
         | cwiz wrote:
         | There's also bevy_math with quite large number of crates
         | depending on it.
        
           | dakom wrote:
           | bevy_math also uses glam, re-exported in the prelude: https:/
           | /github.com/bevyengine/bevy/blob/cc69fdd0c63ea79fda4f...
        
       | lawlessone wrote:
       | Quaternions, we were due our weekly quaternion post.
       | 
       | Nice work by the way!
        
       | ivanjermakov wrote:
       | > Do not run cargo fmt on this code base; the macro used to
       | prevent duplication of code between f32 and f64 modules causes
       | undesirable behavior.
       | 
       | Why not use #[rustfmt::skip]?
        
         | the__alchemist wrote:
         | Good question. I tried marking that on several functions, but
         | still ended up with inappropriate indents, related to the
         | macros. Fortunately, it seems that a (relatively) recent
         | `rustfmt` update resolved this. `fmt` now doesn't work
         | properly, but it doesn't break the indents either.
        
       | gibibit wrote:
       | > For Compatibility with no_std tgts, e.g. embedded, Use the
       | no_std feature.
       | 
       | Note, this is not the recommended way to use Rust feature flags.
       | They are additive and so the correct way to make a `no_std`
       | compatible crate is to have a `std` feature flag that
       | conditionally enables use of the `std` library.
       | 
       | Referring to Effective Rust:
       | 
       | > Note that there's a trap for the unwary here: don't have a
       | no_std feature that disables functionality requiring std (or a
       | no_alloc feature similarly). As explained in Item 26, features
       | need to be additive, and there's no way to combine two users of
       | the crate where one configures no_std and one doesn't--the former
       | will trigger the removal of code that the latter relies on. -
       | https://www.lurklurk.org/effective-rust/no-std.html
        
         | leoedin wrote:
         | I'm a bit confused by this. In practice you can't combine
         | no_std and std within a single project anyway - because then
         | the entire app is relying on the std library, so is no longer
         | no_std. Surely if a user sets no_std as a feature, they expect
         | that to be the case for the entire application?
        
           | junon wrote:
           | std is based on core, the latter of which is available in
           | no_std. Std is thus additive. GP comment is correct; this is
           | how the entire ecosystem structures no_std-compatible crates.
        
           | sapiogram wrote:
           | > In practice you can't combine no_std and std within a
           | single project anyway
           | 
           | You can with Rust features. It allows a library to
           | conditionally compile certain parts of the code, and users of
           | the library can decide which features to enable.
           | 
           | If a library only uses stdlib in code gated by
           | #[feature(std)], users can disable that flag, and use the
           | library in no_std contexts.
        
             | endofreach wrote:
             | Interesting. I have no rust experience yet, so i wonder:
             | while that sounds very cool, how does it look like in the
             | wild? To me it seems like this could quickly turn into a
             | nightmare, if the code is not very well organized /
             | structured. In reality shouldn't this rather be two
             | separate libs?
             | 
             | But maybe i should first learn rust before asking.
        
               | the__alchemist wrote:
               | Anecdotally, the use cases I've had are all clearly in
               | one category or the other: I'm either building firmware
               | for an embedded device, or building a PC application. I'm
               | curious about the overlap cases.
        
               | mlsu wrote:
               | I've used it in the past for creating a shared messaging
               | crate that has packet definitions, handshake logic,
               | serialization/deserialization.
               | 
               | I think with sloppy/complex code it could start to
               | resemble #ifdef PLATFORM complexity if you do a lot
               | inline, but cargo workspaces are a good way to reduce the
               | blast radius.
        
               | gibibit wrote:
               | You're right that an application project (a `binary
               | crate` in Rust parlance) for embedded firmware versus a
               | PC application is very different. But library crates are
               | often usable in both an embedded or system-level context
               | or in a full-fledged desktop GUI application.
               | 
               | Consider these common libraries you might use in either a
               | `std` project (PC application, web microservice) or
               | `no_std` project (embedded microcontroller firmware,
               | bootloader, Linux kernel module, blockchain smart
               | contract):
               | 
               | - data encoding (https://crates.io/crates/base64 for
               | instance),
               | 
               | - hashing (SHA2
               | https://github.com/RustCrypto/hashes/tree/master/sha2),
               | 
               | - data structures (https://github.com/Lokathor/tinyvec)
               | 
               | - time/date manipulation
               | (https://docs.rs/chrono/latest/chrono/)
        
               | burntsushi wrote:
               | As of recently, Jiff with time zone support can now be
               | used in a no_std project. :-)
               | 
               | https://docs.rs/jiff/latest/jiff/tz/index.html#core-only-
               | env...
        
               | sapiogram wrote:
               | It's definitely a tool that must be used carefully, and
               | why informal rules like "features should be strictly
               | additive" exist. I'd say they are very commonly used by
               | large libraries, for example, here's the list of features
               | for Axum, (arguably) Rust's most popular http server:
               | https://docs.rs/axum/latest/axum/#feature-flags
        
           | LoganDark wrote:
           | > In practice you can't combine no_std and std within a
           | single project anyway
           | 
           | This is not in any way the problem.
           | 
           | The reason for having std be the feature instead of having
           | no_std be the feature is because if a dependency unsets the
           | default features because it does not rely on the std feature
           | and then someone else still does rely on the std feature then
           | everything will still work properly.
           | 
           | If no_std is the feature then if a dependency sets the no_std
           | feature because it does not rely on the std features and then
           | someone else does not set no_std because they do rely on the
           | std features then there will be problems because there won't
           | be any ability for the someone else to unset the no_std
           | feature that was specified by the first dependency.
        
         | the__alchemist wrote:
         | This is great info! I tried using that approach, but had
         | trouble enabling the `num_traits/libm` dependency on no_std
         | only. Does anyone know if that's possible?
        
       | aquarin wrote:
       | Very nice. Are there any test covering the functionality?
        
         | m00dy wrote:
         | unfortunately, there are no tests in it
        
       | moron4hire wrote:
       | let mut d = a.dot(b);       d.normalize();
       | 
       | I think I found a bug in your readme. Dot product should return a
       | scalar. I don't know Rust at all, but I've never met any
       | languages that had a normalize method for scalars.
        
         | the__alchemist wrote:
         | You're absolutely correct
        
       | Keyframe wrote:
       | I admit I haven't gone yet past the README, but I did a fair
       | share of such programming in the past and there's always the same
       | use-case presented which is fine for clarity, but.. ask yourself
       | when is it that you're doing one of each (transforms,
       | normalizations, etc.)? General use-case is many (MANY) at once,
       | and writing an optimized code for both is vastly different.
        
         | the__alchemist wrote:
         | Concur on this. Usually reaching for an approximation algorithm
         | (FMM, Barnes Hut etc), and/or serialization and sending to a
         | CUDA kernel. And generally using Rayon to parallelize if not on
         | CUDA. I'm curious how to explore the space on CPU optimization
         | (SIMD, SOA/AOS etc), but don't know anything about it.
        
           | Keyframe wrote:
           | _I 'm curious how to explore the space on CPU optimization
           | (SIMD, SOA/AOS etc), but don't know anything about it._
           | 
           | as with anything in that regard. profile, profile, profile.
           | valgrind, check cache misses and profile. calculate
           | theoretical throughput of a cpu you're working on, like
           | actual bandwidth of reading/writing RAM, with and without
           | caching, and that's your high post; to try to get as close as
           | possible to those limits. If you want to start with that, you
           | can do just that, simple reads/writes and profile and then
           | introduce functions and structures instead and try to reclaim
           | speed as much as possible. graphs over profiling always help,
           | even better graphs over profiling on commits or PRs so you
           | can tell how you're progressing. But that's just like my
           | opinion, man. No right/wrong way, profiling always tells the
           | truth in the end.
           | 
           | tl;dr; read ops for cpu; profile.
        
       ___________________________________________________________________
       (page generated 2025-03-07 23:01 UTC)