SIMD: Am I holding it wrong?

I'm new to this, so any information is much appreciated.

I've created a class like so:

class ABunchOfVectors<Vector: SIMD> where Vector.Scalar: FloatingPoint {

var myVectors: [Vector]

}

I'd like to be able to pass any SIMD type to that and do things like calculate distances between them, but I'm finding that Xcode can't find the appropriate method and instead I'm defining my own Euclidian distance function like so:

private func distance(_ x: Vector, _ y: Vector) -> Vector.Scalar

Is that right? I suppose I could also do this by extending the SIMD protocol, but it feels to me like something that should be built-in, and so I'm wondering if I'm missing something obvious.

1 Like

SIMD has nothing to do with geometry, it's merely a type facilitating hardware-accelerated Single Instruction Multiple Data (hence, SIMD) operations. Its application in the field of geometry is merely a single use case that arguably doesn't belong in the standard library.
You'll need to either implement your own or find an existing solution.

4 Likes

Is this what you're looking for?

EDIT:

[REMOVED]

EDIT 2:

Looks like this was already discussed here.

EDIT 3:

Disregard the above. You don't need to implement it yourself, it exists here. You just need to import simd.

2 Likes

Note that the simd module is part of Apple's SDK, rather than the Swift open source project (and so doesn't exist on non-Apple platforms). That may or may not be a problem for you.

2 Likes

Thanks all. This is helpful, if only to confirm that I'm not missing anything obvious =)

Maybe to narrow down the discussion, I did find the Type-specific distance functions, where I'm running into a hurdle (on Apple platforms) is that generic functions over any SIMD type aren't able to figure out which of those functions to call, and so I'm essentially left re-implementing them in a general case.

Maybe this isn't a common issue, but I did note this thread in which I think @scanon was essentially saying this was a TBD feature. Is that still the case?

Thanks for your help so far!

1 Like

Looking from the outside, I would be surprised if there were any methods on SIMD which weren't implemented with SIMD instructions (perhaps not on all architectures, but on at least one of them).

My mental model for that protocol is that it's a thin hardware abstraction layer, and that any algorithms that use it are defined on the layer above, in some numerics library.

3 Likes

Writing generic code against SIMD is pretty fraught--the whole point of SIMD types is unlocking performance optimizations, but if the compiler cannot see through all your generic code and specialize everything, you will fall off a pretty nasty performance cliff. It's fine to write small functions generically and call them from concrete contexts, but we pretty often see people trying build big systems that are entirely generic (usually people coming from a C++ background who expect generics to behave like templates), and it's very easy to end up going through protocol dispatch for every operation when you do this.

So the first thing I would ask is: do you actually need this to be generic, or are you just writing it that way out of C++-derived habit?

This in particular makes me think that you're approaching the problem as though you were using C++ templates, because this is precisely how they work. This is not how generics in Swift work, and you'll save yourself a lot of pain if you learn about the distinction before writing a lot of code.

4 Likes

my more actionable advice is to @frozen + @inlinable everything that touches the floating point generics, which is how you achieve C++ templating in swift.

it is smelly but unavoidable in mathy stuff like geometry code. the alternative is to put all of your mathy code into one module.

1 Like

The other alternative is to just write concrete code if you don't actually need generics, which applies to most people who aren't writing general-purpose libraries =)

So long as you can stay on the happy path, this is sort-of true, but if one existential or unspecialized generic entry point slips in somewhere, then everything thereafter will go through protocol dispatch. (And you still don't get the ability to call concrete functions from your generic code with magic "hey, if it compiles after substitution, then it's allowed" template behavior, which is what OP was asking for)

2 Likes

Thanks @scanon for the prompt to dig more into Swift generics. I'll keep at it, but I think what I'm after is static dispatch.

So in my case, I'm taking a bunch of high-dimensional data and reducing it down to something in the ballpark of SIMD vectors, let's say between 2 and 10 dimensions. I'd then like to do some ML and graphics work with that data, which is why I reached for SIMD -- it has a lot of this already pre-built, and if I get performance gains all the better.

The issue is that I don't know which SIMD values I'll need. I will need SIMD2 for sure, and likely SIMD4 or SIMD6, but for sure I'll need more than one. So it felt like a natural place to reach for some generic code.

All of these also have a distance function like func distance(_ x: SIMDX, _ y: SIMDX) -> Double what I was hoping for was a way for the compiler to look at the type and infer which of those functions it needs to call, but since they're not part of the SIMD protocol I don't think this is possible?

Thanks @taylorswift for the pointer, I'll read up on those.

1 Like

I should add that I spent some list last night reading the SIMD proposal and much of this use case falls under the "vector class library" use case. I am trying to avoid baking my own, worrying about conversion, etc.

oh my, this brings back a memory of a time i worked on a spherical geo project that we decided to write in Float thinking it would be easier to interface with the GPU-side of the project that only spoke Float. then halfway through we discovered Float was nowhere near precise enough for spherical algorithms so we had to migrate the whole thing to Double. which wasn’t that bad since it only involved a bunch of find-and-replaces (FloatVector –> DoubleVector, etc) and rewrapping some overflowing lines. but my point is it's hard to anticipate ahead of time what floating point specialization you need unless you've already got a ton of experience with numerics. anyway, just a random anecdote.

yeah, this is definitely still a major pain point with swift generics.

4 Likes

In case it helps anyone else who finds this thread, I think I've settled on something like this, where I provide a generic method for SIMD, but link to specific (and hopefully more efficient?) methods for SIMD2/3/4.

Full code dump
extension SIMD where Self.Scalar: BinaryFloatingPoint {
    
    // A default Euclidian distance for SIMD where more efficient functions aren't provided
    public func distance(_ x: Self, _ y: Self) -> Self.Scalar {
        let difference = x - y
        var distance = Self.Scalar(0)
        for i in difference.indices {
            distance += difference[i] * difference[i]
        }
        return distance.squareRoot()
    }
}

extension SIMD2<Double> {   
    @inlinable
    public func distance(_ x: Self, _ y: Self) -> Double {
        return simd.distance(x, y)
    }
}


extension SIMD3<Double> {    
    @inlinable
    public func distance(_ x: Self, _ y: Self) -> Double {
        return simd.distance(x, y)
    }
}

extension SIMD4<Double> {

    @inlinable
    public func distance(_ x: Self, _ y: Self) -> Double {
        return simd.distance(x, y)
    }
    
}

extension SIMD2<Float> {
    
    @inlinable
    public func distance(_ x: Self, _ y: Self) -> Float {
        return simd.distance(x, y)
    }
    
}

extension SIMD3<Float> {
    
    @inlinable
    public func distance(_ x: Self, _ y: Self) -> Float {
        return simd.distance(x, y)
    }
    
}

extension SIMD4<Float> {
    
    @inlinable
    public func distance(_ x: Self, _ y: Self) -> Float {
        return simd.distance(x, y)
    }
    
}

For the general case, which I think will be specialized for concrete cases, you can write a more efficient implementation

extension SIMD where Scalar: FloatingPoint {
    public func distance(_ x: Self, _ y: Self) -> Self.Scalar {
        let difference = x - y
        let squared = difference * difference
        return squared.sum().squareRoot() 
   } 
}
1 Like

Thank you @MarSe32m !