Edit Sorry in advance for the compressed stream of thoughts below. It’s 1:30am on Christmas Eve, I got linked here from another location, and I was/am somewhat frustrated about some of the perceived misinformation and lack of familiarity that tends to coincide with these topics. It’s not the fault of anyone here, it’s just one of those cases that I regularly need to address. It is much as I often have to explain the basics of how IEEE 754 floating-point works and it being deterministic, how you don’t need to use double
to solve precision issues, how chunking and floating origin systems work, how fixed-point doesn’t solve underlying problems and introduces additional problems instead, etc. – I end up reiterating a lot of the same information many times in many places throughout the year and repeating it year after year.
=========================
Types like Vector2/3/4 and Matrix3x3/4x4 don’t get templated because they are highly specific, specialized, and often “primitive” types. They get their own static types that are used in these highly specific scenarios and which are typically hand-tuned to use very explicit paths that are designed for and around the needs of graphics, general image processing, and similar scenarios. These types then get used much like other primitives (such as int
, float
, bool
, etc) to build other specialized types representing the structured data.
This is why you find that libraries like DirectX Math
or GLM
(OpenGL Mathematics) don’t really use templates in the way being described/considered. GLM itself does use some level of templating and if you naively look at the code you might presume that it is using this to optimize. However, if you dig deeper you will find that this general support is actually quite lacking and that the main library is actually all built around explicit specialization of the core types/sizes; such that they’ve functionally defined multiple unique types with no shared logic. The underlying vec<length_t, typename, qualifier>
(and equivalent for mat
) is actually really just this almost unusable interface (trait-like definition), isn’t used for storage, or other considerations. You can functionally define “the same thing” in .NET (RyuJIT) via an actual generic interface, a TSelf
generic, and relying on inlining/devirtualization or generic specialization for value types.
– Notably this general specialization support, ability to use interfaces and modern language features, etc also means that real world usages do not incur additional fields, do not incur lower iteration performance, etc. You can build something (in .NET) that has the same or even better codegen than C/C++ (as you’re not targeting a lowest common machine by default).
Tensor libraries on the other hand are more general purpose. They are typically working with non-static data where the size, shape, sparseness of the data, and other considerations are never decided by the developer (either the tensor author or the tensor consumer). Instead they are often dictated by the source data set which is often external to the library and specified by the data scientist or in the case of general ML applications by the arbitrary input provided by the user.
Because they are designed to be general purpose and not understand the size or other information, it is incredibly atypical to use templates for representing size information (as it is never statically known). The allocations are generally large enough that any additional field cost used to track the size, shape, sparseness is negligible. The data is typically sliced out of larger memory, often breaking it down to the largest contiguous “row” of information, accounting for where the shape matters vs is irrelevant. Where if there is a core/common size where extra optimizations are meaningful to your app, you can do the necessary dynamic check and then optimize accordingly (but its often not meaningful).
=========================
You ultimately have different types for different scenarios and goals. .NET itself has in-box Vector, Matrix, Plane, Quaternion, and other types for graphics scenarios in the System.Numerics namespace. .NET is then currently building its own set of “tensor primitives” and a general tensor interchange type in the System.Numerics.Tensors namespace, some of which are stable and others which are still experimental.
You’re going to be hard pressed to build something that is better accelerated or handled across all the relevant scenarios, especially when considering the range of x86
, x64
, Arm64
, WASM
, and potential other future targets.
Where there are places that need additional APIs, defining them as extensions or utility methods on top of these existing APIs will give you the best performance and compatibility. Opening issues so that we can add them officially in box, where relevant, is also goodness (dotnet/runtime on GitHub). Noting that we can’t add “everything”, nor is everything applicable enough to warrant a central in-box method.
Providing additional interchange concepts can also be done where relevant. On the eventual radar is some kind of Vector2<T>
and related set of interfaces so that you can have vectors of half/double. Potentially some kind of similar Point2<T>
so that you can have similar concepts (but not all the same APIs) for types like int
and byte
as well might be possible – Noting that Vector2<int>
despite being a common term to see in some libraries in many ways doesn’t make “sense” because core operations like Normalize
or Length/Magnitude
which are foundational to Euclidean Vectors do not work with integer types. The integer usage is generally as a basic Euclidean Point or other basic tuple, which is a related but not quite identical concept and where the set of functions exposed differs. – This really gets into the differences between scalar/vector/matrix/tensor
the mathematical concepts vaguely representing programming arrays or collections and vector/matrix
the geometry (typically Euclidean) concepts that represent a concrete piece of information around a coordinate system.
I don’t think that some IVector2<TSelf, T>
or similar interface is necessary. It is sufficient to simply define a conversion operator that does what will typically be a bitcast (such as via Unsafe.BitCast
) in the cases you have one type of Vector2
and need to reinterpret as another Vector2
(such as interchanging between the .NET and Unity vector types).