Skip to content

Extend the vector types to have char as a supported element type #127611

@tannergooding

Description

@tannergooding

Rationale

Code working with strings or string like data is frequently vectorized for performance. However, today this requires explicit checks and unsafe code to produce a Vector128<ushort> from what is typically a ReadOnlySpan<char> based input.

Given that char is part of the generic math feature (implements IBinaryInteger<char>) and has always supported the full set of arithmetic operators in C# (i.e. someChar /= someChar works), it is proposed we just relax the restriction and allow Vector128<char>.IsSupported to report true.

Doing this is mostly just relaxing an import restriction in the JIT.

API Proposal

We can get away with as few as 1 API per type (AsChar). This allows all generic operations to work and only minimally excludes a handful of additional APIs which cannot be generic due to special considerations (such as changing type as part of their operation or for specific arguments). Users could then still access this functionality via AsChar() and AsUInt16().

public static partial class Vector128
{
    public static Vector128<char> AsChar<T>(this Vector128<T> vector);
}

// Repeat for Vector64, Vector256, Vector512, and Vector

However, doing so is a bit "unusual" and so for clarity the other APIs to fully match other T would be:

public static partial class Vector128
{
    // A user doing `Create('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h')` actually already binds to `Vector128<ushort>` today
    public static Vector128<char> Create(char e0, char e1, char e2, char e3, char e4, char e5, char e6, char e7);

    // We have `M<T>(...)` variants, the non-generic versions are older and so these are "consistency"
    public static Vector128<char> Create(char value);
    public static Vector128<char> Create(Vector64<char> lower, Vector64<char> upper);
    public static Vector128<char> CreateScalar(char value);
    public static Vector128<char> CreateScalarUnsafe(char value);

    // Can be achieved with `AsUInt16()`
    public static Vector128<byte> Narrow(Vector128<char> lower, Vector128<char> upper);
    public static Vector128<byte> NarrowWithSaturation(Vector128<char> lower, Vector128<char> upper);

    // Operators exist, these are just named variants
    public static Vector128<char> ShiftLeft(Vector128<char> vector, int shiftCount);
    public static Vector128<char> ShiftRightLogical(Vector128<char> vector, int shiftCount);

    // Indices need to explicitly be integers (same for `float`/`double`)
    public static Vectore128<char> Shuffle(Vector128<char> vector, Vector128<ushort> indices);
    public static Vectore128<char> ShuffleNative(Vector128<char> vector, Vector128<ushort> indices);

    // Can be achieved with `AsUInt16()`, notably no convention for `byte->char` exists so `WidenToChar` or something may also be warranted
    public static (Vector128<uint> Lower, Vector128<uint> Upper) Widen(Vector128<char> vector);
    public static Vector128<uint> WidenLower(Vector128<char> vector);
    public static Vector128<uint> WidenUpper(Vector128<char> vector);
}

// Repeat for Vector64, Vector256, Vector512, and Vector

Alternative Proposal

We could instead say that Vector128<char> is unsupported and only expose APIs for loading/storing as char and have the vector it produces be ushort. We already have some such helpers internally. Much as before, we can also get away with fewer APIs if we want some cases to do something different.

public static partial class Vector128
{
    public static void CopyTo<T>(this System.Runtime.Intrinsics.Vector128<ushort> vector, Span<char> destination);

    public static System.Runtime.Intrinsics.Vector128<ushort> Create(ReadOnlySpan<char> values);
}

// Repeat for Vector64, Vector256, Vector512, and Vector

However, the set of APIs to match other T would be:

public static partial class Vector128
{
    // Can just use the Span overload
    public static void CopyTo<T>(this Vector128<T> vector, char[] destination);
    public static void CopyTo<T>(this Vector128<T> vector, char[] destination, int startIndex);
    public static Vector128<ushort> Create(char[] values);
    public static Vector128<ushort> Create(char[] values, int index);

    // Casting the pointer is trivial, the API is `caller unsafe`
    public static unsafe Vector128<ushort> Load(char* source);
    public static unsafe Vector128<ushort> LoadAligned(char* source);
    public static unsafe Vector128<ushort> LoadAlignedNonTemporal(char* source);
    public static unsafe void Store(this Vector128<ushort> source, char* destination);
    public static unsafe void StoreAligned(this Vector128<ushort> source, char* destination);
    public static unsafe void StoreAlignedNonTemporal(this Vector128<ushort> source, char* destination);

    // Can use `Unsafe.AsRef`, the API will be `caller unsafe`
    public static unsafe Vector128<ushort> LoadUnsafe(ref readonly char source);
    public static unsafe Vector128<ushort> LoadUnsafe(ref readonly char source, nuint elementOffset);
    public static unsafe void StoreUnsafe(this Vector128<ushort> source, ref char destination);
    public static unsafe void StoreUnsafe(this Vector128<ushort> source, ref char destination, nuint elementOffset);
}

// Repeat for Vector64, Vector256, Vector512, and Vector

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions