rust-lang / portable-simd

The testing ground for the future of portable SIMD in Rust
Apache License 2.0
892 stars 80 forks source link

Use a more trait-oriented design #228

Closed workingjubilee closed 1 year ago

workingjubilee commented 2 years ago

We have coalesced consensus around a more direct application of using traits for the type-argument-specific functions on std::simd::Simd and finding other ways to resolve the too-many-imports problem, at least for now. While it is inconvenient to have to import such traits, "making imports more manageable" is something that should probably be addressed at a slightly higher design level than within this library's types (since it's of relevance to all of Rust).

( Obviously, if people really want these traits in scope always, they can nominate them for the prelude as-is, though a solution that is less dependent on "being std", like inherent traits, would be nice. )

Lokathor commented 2 years ago

use core::simd::prelude::*; doesn't seem particular burdensome until then.

workingjubilee commented 2 years ago

@Urgau asked:

Hi, I was wondering why this crate choose to go with a trait-oriented design and all the complication with it instead of going with the primitive/struct and macros route like core is doing for uX, iX and fX ?

Short Answer

We tried that and it proved fairly unwieldy and subpar.

Long Answer

First and foremost: This is all still an experiment on nightly, so we remain entitled to rewrite everything. We will not exercise this entitlement frivolously, but some amount of tinkering is not frivolous (and, for instance, code using the type aliases has often remained stable in spite of massive internal changes).

But it turns out the way that the core primitive types work

Please see https://github.com/rust-lang/rfcs/pull/2581, which proposes generic integers as a way to deal with some of the existing problems. In essence, it is not actually clear that having something like num-traits in std is a Bad Idea, and T-libs has expressed at least some positive sentiment towards it recently.

But we still must provide methods that are, at minimum:

This means we can either use struct Simd<T, N> as the "base container", or provide an impl simd::Vector for all types we use... or we can duplicate everything for every type. Likewise, we can have struct SimdFloat<T, N> or Simd<T, N> with impl simd::Float... or we can have struct SimdF32<N> and SimdF64<N> and have to duplicate everything. With floats that's almost reasonable, but it would not be so for the vectors-of-integers types.

The principle "entities shall not be multiplied beyond necessity" (also known as Occam's razor, and often misquoted as something like "the simplest solution is often the best") undergirds much of logic, math, and even user experience design. This is also referred to as "Don't Repeat Yourself" in programming. The problem is that using macros still constitutes a certain amount of "repeating yourself", including in the form of documentation. And that is, at least, my major concern:

The fact that two integers implement the same methods because they have the same macro applied to them is an implementation detail, one that is not guaranteed to be uniformly true. This came up with widening_mul: while that (also experimental) method now returns a tuple, it previously might have returned an integer of the next greater size, which obviously was not also true of u128. That we might allow this to happen is a nontrivial concern for Simd types, because various hardware does in fact do exactly that.

A trait forms a contract that the methods are indeed implemented on all entities that implement that trait, and at least notionally implement the "spec" of that trait. For a trait that is generic over u8, u16, u32, u64, and u128, this means you do not have to check that they still implement the same methods: you know they do. This means you do not have to concern yourself with details about those integers, you only interact with one entity: a "uint". While it is possible generic integers might change our approach significantly in multiple ways, they are not here at the moment.

So, not using traits causes this library to add ~15 entities instead of, say, ~5.

And it also, incidentally, when using it with Simd<T, N>, would cause a giant mash of all the numeric types, including many with incompatible methods, being documented as one big pile. That's at least in theory fixable in terms of sheer duplication but organizationally is a bit trickier. Logically, traits provide a cleaner separation, and if the real issue is documentation not being obvious and we need to alter how rustdoc behaves, it's far easier to improve visibility of a trait impl on a type's rustdoc page instead of deduplicating method impls based on various parameters on the same type.

calebzulawski commented 1 year ago

After #350, no element-type-specific functions remain on Simd. #344 added a prelude which assists in importing all of the traits. I think we are officially trait-oriented, so I'm closing this issue.