<p>C and C++ have no vector types or operations but many compilers offer their own non-portable extensions such as <a href="https://stackoverflow.com/q/38750994/16217248"><code>__attribute__((__ext_vector_type__()))</code></a>. The issue is vector types corresponding to vector registers and specific CPU vector instructions are not portable and the semantics vary for different CPUs.</p>
<p>However all SIMD implementations are conceptually the same: Applying the same operation to many different values at a time. What semantics could a language include that supports vectorization or constructs that could be trivially vectorized by a compiler and would be more or less independent of the underlying SIMD semantics of the CPU? What would be a good abstraction for vector operations that a language could provide?</p>

<syntax><optimization><simd>
How can a programming language support vectorization portably?
CC BY-SA 4.0
2023-08-25T00:42:03.167Z
I don't know if any of the documentation has been archived online, but you might want to search for the languages developed at Thinking Machines Corporation for the massively parallel Connection Machine: `C*`, `*Lisp`, `CM Lisp`, and `CM Fortran`. While we had a web site, this was the early days of the web and we didn't have tools for moving this stuff online at the time.
Barmar
<p><strong>TL;DR: This is very much an open question.</strong></p>
<h3>Challenges</h3>
<p>There are two challenges:</p>
<ol>
<li>The varying size of registers: some CPU only allow specific fixed-sizes (Intel CPUs go 128 bits, 256 bits, 512 bits) while others allow variable-sizes (up to a maximum).</li>
<li>Various CPU families, and various CPUs within a family, have different sets of instructions, and while vector addition is a mainstay and fairly uniform, shuffle or lookup-table instructions are quite disparate.</li>
</ol>
<p>The problem, therefore, is that given the incredible diversity of the instructions, it is hard to come up with an abstraction.</p>
<h3>Partial Handling of Size Differences</h3>
<p>Handling the varying sizes of common instructions (vector addition, vector multiplication) is perhaps the simplest task.</p>
<p>Apart from built-in language/compiler features, the use of generics allows the user to express a transformation <em>regardless</em> of the vector size. Combined with a library that handles the selection of the size -- with a mix of compile-time and run-time -- this works fairly well.</p>
<p>As an example consider the <a href="https://github.com/AdamNiederer/faster" rel="nofollow noreferrer"><code>faster</code></a> library (Rust):</p>
<pre class="lang-rust prettyprint-override"><code>let lots_of_3s = (&amp;[-123.456f32; 128][..]).simd_iter()
    .simd_map(f32s(0.0), |v| {
        f32s(9.0) * v.abs().sqrt().rsqrt().ceil().sqrt() - f32s(4.0) - f32s(2.0)
    })
    .scalar_collect();
</code></pre>
<p>And note how the users is manipulating <code>f32s</code> without specifying the number of lanes.</p>
<p>This works because the language provides:</p>
<ul>
<li>Generics.</li>
<li>Compile-time detection of the set of number of lanes potentially available.</li>
<li>Run-time detection (standard library) of the set of number of lanes actually available.</li>
<li>Intrinsics, mapping to the desired SIMD instructions.</li>
</ul>
<p>Unfortunately, this solution is only partial because it only uses &quot;input-driven&quot; number of lanes picking. If the user wished to avail themselves from table-lookup instructions, then it would be harder to abstract that -- as the very way the table is populated depends on the number of lanes actually available.</p>
<p>In such cases, the user essentially needs to construct multiple code-paths, each for a given number of lanes, or restrict execution to the lowest common denominator. And even then, the user should beware of semantic differences between instructions sets.</p>
<h3>Language (&amp; Run-time)</h3>
<p>In the end, a power-user will, today, outshine any auto-vectorization... because a power-user can use SIMD instructions in ways that boggle the mind (see simdjson) and no compiler has optimizations that are that sophisticated.</p>
<p>To support a power-user, then a language should provide:</p>
<ul>
<li>Intrinsics: so that the power-user can directly call the vector instruction.</li>
<li>Compile-time/Run-time detection/selection facilities: so that the power-user can choose which code-path to use based on the current CPU model (and generic CPU family) and possibly refuse to use older models.</li>
</ul>

CC BY-SA 4.0
2023-09-08T14:34:49.253Z
2023-09-08T14:34:49.253Z
Matthieu M.
CC BY-SA 4.0
2023-09-08T15:32:15.250Z
Agreed, this is **very much** not a solved problem. You can also see this from the many ISA extensions the processor designers have been cooking up.
Martin Berger
<p>First, you make it part of the language, so it isn't non-portable because some compilers don't support it or use different syntax.</p>
<p>Now lets say processor A has vector registers containing two single precision numbers, while processors B, C and D have vector registers with 4, 8, or 16 single precision numbers. How do you do this portably? You can allow declaring a type &quot;array of 16 single precision numbers&quot; or &quot;array of 17 single precision numbers&quot; everywhere and the compiler produces for example code for 8, 4, 2 or 1 operations, or for one additional operation.</p>
<p>The programmer can look at the size of a register and create different instructions depending on the register size.</p>
<p>And every processor has some instructions that don't fit into any scheme in the language, in that case there could be intrinsic functions that work at differnt speeds depending on the processor.</p>

CC BY-SA 4.0
2023-08-28T23:04:10.373Z
2023-08-28T23:04:10.373Z
gnasher729
<p>The way GLSL does SIMD is by having vectorized primitives (float3, float4, etc) that you can do operations on, or access individual elements. I figure a templatized version of it that can expand to however many elements the user wants is probably the best way, and then the compiler can reduce that to however many elements the CPU can vectorize.</p>
<p>One example is <code>std::experimental::simd&lt;&gt;</code> in C++ (experimental) <a href="https://en.cppreference.com/w/cpp/experimental/simd" rel="nofollow noreferrer">SIMD library</a>:</p>
<blockquote>
<p>The SIMD library provides portable types for explicitly stating data-parallelism and structuring data for more efficient SIMD access.</p>
</blockquote>

CC BY-SA 4.0
2023-05-16T17:49:08.427Z
2023-08-23T16:38:19.677Z
2023-08-23T16:38:19.677Z
kouta-kun
CC BY-SA 4.0
2023-05-16T18:11:28.870Z
“a templatized version of that…” Like C# [`Vector<T>`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector-1?view=net-7.0)?
Bbrk24
CC BY-SA 4.0
2023-05-16T18:12:35.570Z
I was thinking of it being templated on the vector length instead of the type, so there could be a `FloatVec<C>` type that can be used as `FloatVec<4> | FloatVec<5>` etc. That seems to me like the best way to support it compiler side instead of passing that work on to the standard library like I believe C# does.
kouta-kun
CC BY-SA 4.0
2023-08-29T01:49:17.603Z
These days, shader `float3`/etc types are not typically compiled to SIMD operations in the way you might expect. Instead, shader compilers exploit the fact that the program is written in terms of a single vertex/fragment/etc but intended to run on a large number of them independently. The SIMD lanes are used to parallelize across those, rather than anything within the source program.

This is typically referred to as "SPMD."
rpjohnst
<p>Probably the most portable way to do this is to leverage the very sophisticated optimisations, including automatic vectorisation, provided by established compiler backends such as LLVM. For the vast majority of language implementors, compiling to a suitable IR and then letting a third-party backend perform the low-level optimisations will be a much better trade-off of implementor effort to program performance, than implementing such optimisations oneself, especially if there is a need to support multiple target architectures (i.e. portability).</p>
<p>For example, here's a Rust function which naively adds four floats:</p>
<pre><code>pub struct Vec4 {
    x: f32, y: f32, z: f32, w: f32,
}

impl Vec4 {
    pub fn add(self, other: Self) -&gt; Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
            w: self.w + other.w,
        }
    }
}
</code></pre>
<p>Rust's compiler uses LLVM as a backend. Here's the output from <a href="https://godbolt.org/z/Trvv3zYK1" rel="noreferrer">Godbolt Compiler Explorer</a>, with optimisations enabled:</p>
<pre><code>example::Vec4::add:
        mov     rax, rdi
        movups  xmm0, xmmword ptr [rsi]
        movups  xmm1, xmmword ptr [rdx]
        addps   xmm1, xmm0
        movups  xmmword ptr [rdi], xmm1
        ret
</code></pre>
<p>The addition is done by the <code>addps</code> instruction, which:</p>
<blockquote>
<p>Adds four, eight or sixteen packed single-precision floating-point values from the first source operand with the second source operand, and stores the packed single-precision floating-point result in the destination operand.</p>
</blockquote>
<p>So the programmer didn't need to give any extra context or hints in the source code, the LLVM backend was smart enough to vectorise it anyway.</p>

CC BY-SA 4.0
2023-05-16T21:34:40.723Z
2023-05-16T21:41:43.757Z
2023-05-16T21:41:43.757Z
kaya3
CC BY-SA 4.0
2023-09-08T14:16:45.730Z
I would not necessarily say this is portable, as unfortunately depending on the whims of the optimizer is rather brittle. That is, while LLVM may auto-vectorize this version of the code: it could fail to auto-vectorize the next version, GCC could fail to auto-vectorize this version, and of course it could also fail on some platforms...
Matthieu M.
CC BY-SA 4.0
2023-09-08T17:13:34.620Z
@MatthieuM. That's a fair point, but I think for major compiler backends like LLVM and GCC, the failure to vectorise this kind of code (when compiling with suitable optimisation flags enabled) would be considered a bug. I don't have enough experience with either to confirm this, but I'd be surprised if that kind of optimisation wasn't tested for in those projects' automated tests.
kaya3
CC BY-SA 4.0
2023-09-18T15:36:04.083Z
I wouldn't regard this as a reliable means of allowing programmers to write efficient code in implementation-agnostic fashion.  If a program would require that a compiler find a particular optimization in order to yield good performance, there should be a means of explicitly inviting that optimization.  In many cases, performing optimizing transforms is very easy compared with identifying cases where they will be worthwhile, and validating that the transforms will not affect behavior *in ways contrary to application requirements*.
supercat
CC BY-SA 4.0
2023-09-18T15:41:35.123Z
If a programming language has a construct which specifies that a range of code should be executed once for each value of `i` within a certain range, but the loop iterations may be performed in any order, and a `break` may skip any, all, or none, of the remaining iterations, a compiler will be able to transform to use vector or parallel operations much more easily, safely, and effectively, than it could parallelize a "for" loop written via in conventional fashion.
supercat
<h1><a href="https://en.wikipedia.org/wiki/Array_programming" rel="noreferrer">Array Programming</a></h1>
<p>One possible way is to have arrays similar to C++ <a href="https://en.cppreference.com/w/cpp/numeric/valarray" rel="noreferrer"><code>std::valarray</code></a> or Fortran arrays. Including semantics to perform operations on an entire array would be higher level than the compiler-specific SIMD types but could easily be implemented with SIMD.</p>
<p>An example of array programming semantics in a C-style language:</p>
<pre><code>int array_1[4] = {1, 2, 3, 4};
int array_2[4] = {5, 6, 7, 8};
int array_3 = array_1+array_2; // {6, 8, 10, 12}
// Array-wise operations could be trivially identified and implemented with SIMD instructions
</code></pre>

CC BY-SA 4.0
2023-05-17T00:15:58.943Z
2023-05-20T02:53:35.927Z
2023-05-20T02:53:35.927Z
CPlus
CC BY-SA 4.0
2023-05-17T00:55:55.533Z
This "hypothetical language", of course, is older than a lot of people here: Fortran 77.
Pseudonym
<p>One important consideration is how your language handles <em>aliasing</em>.  This is a major killer of auto-vectorization opportunities for C and C++.</p>
<p>For example, in the following function:</p>
<pre><code>void add(float *out, const float *a, const float *b, size_t count) {
    for (size_t i = 0; i &lt; count; i++) {
        out[i] = a[i] + b[i];
    }
}
</code></pre>
<p>the compiler must not emit the obvious vectorized code, because the caller is allowed to pass a pointer for <code>out</code> that overlaps one of the input arrays.  Sometimes they can vectorize by adding code to compare the pointers for overlap, and falling back to a naive scalar implementation if they do.</p>
<p>C99 tried to ameliorate this with <code>restrict</code>, with which you can promise the compiler that the buffers will not overlap.  But its definition involves complicated semantics for when an access occurs indirectly &quot;through&quot; the pointer <code>out</code>.  Moreover, it's up to you, the programmer, to ensure that all callers keep that promise, on pain of undefined behavior, and the compiler can't necessarily detect violations.</p>
<p>On the other hand, in Rust, the equivalent code can be auto-vectorized easily and safely.  You have to borrow the output array in order to pass its reference to <code>add</code>, and this guarantees that no other reference (such as to the input arrays) can alias it.  The compiler can and does ensure that callers keep this promise.</p>

CC BY-SA 4.0
2023-08-24T03:21:55.427Z
2023-08-24T03:21:55.427Z
Nate Eldredge
CC BY-SA 4.0
2023-09-08T15:31:15.873Z
Good point, but in some cases the input and output arrays are the same, or overlapping. The programmer needs to ensure that the algorithm is correct in both cases, pointers aliasing and not aliasing. The compiler typically does alias analysis and emits different code for the *"may-alias"* and *"cannot-alias"* cases.
Martin Berger
CC BY-SA 4.0
2023-09-08T15:33:27.540Z
@MartinBerger: Yes, that's exactly the point I am making.
Nate Eldredge
CC BY-SA 4.0
2023-09-18T15:44:14.130Z
@NateEldredge: The C99 rules could have been simple if it recognized a three-way split of objects definitely based on P, objects definitely not based on P, and objects that might be based on P, with aliasing restricted only between the first two, and accepting that not all objects could be classified into the first two categories (though any and all objects could safely be classified into the third).  Worse than the complexity, however, is the fact that the rules even as implemented have weird and bizarre corner cases which result in equality comparisons having side effects.
supercat
CC BY-SA 4.0
2023-09-18T16:08:04.323Z
@MartinBerger: Indeed, many optimizations rely upon a compiler's ability to safely assume that `arr1[i]` will not alias `arr2[i+n]` for a constant non-zero `n`.  Such an assumption might not hold if `arr1` and `arr2` are different and identify overlapping regions of storage, but would hold in both the "pointers equal" and "storage is disjoint" cases, without code needing to distinguish between them.  Worse, the way clang and gcc process `restrict` means that even code that specially handles the "pointers are equal" case may be transformed in breaking fashion.
supercat
# How can a programming language support vectorization portably?
Posted by: username_0 on 2023-05-16
C and C++ have no vector types or operations but many compilers offer their own non-portable extensions such as [`__attribute__((__ext_vector_type__()))`](https://stackoverflow.com/q/38750994/16217248). The issue is vector types corresponding to vector registers and specific CPU vector instructions are not portable and the semantics vary for different CPUs.

However all SIMD implementations are conceptually the same: Applying the same operation to many different values at a time. What semantics could a language include that supports vectorization or constructs that could be trivially vectorized by a compiler and would be more or less independent of the underlying SIMD semantics of the CPU? What would be a good abstraction for vector operations that a language could provide?
**Tags:** syntax optimization simd
> **Comments:**
> **username_1**: I don't know if any of the documentation has been archived online, but you might want to search for the languages developed at Thinking Machines Corporation for the massively parallel Connection Machine: `C*`, `*Lisp`, `CM Lisp`, and `CM Fortran`. While we had a web site, this was the early days of the web and we didn't have tools for moving this stuff online at the time.  
---
## 6 answers
### Answer by username_5 on 2023-05-16. Score: 7 | Accepted answer
The way GLSL does SIMD is by having vectorized primitives (float3, float4, etc) that you can do operations on, or access individual elements. I figure a templatized version of it that can expand to however many elements the user wants is probably the best way, and then the compiler can reduce that to however many elements the CPU can vectorize.

One example is `std::experimental::simd<>` in C++ (experimental) [SIMD library](https://en.cppreference.com/w/cpp/experimental/simd):

> The SIMD library provides portable types for explicitly stating data-parallelism and structuring data for more efficient SIMD access.
> **Comments:**
> **username_6** (2023-05-16): “a templatized version of that…” Like C# [`Vector<T>`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector-1?view=net-7.0)?  
> **username_5** (2023-05-16): I was thinking of it being templated on the vector length instead of the type, so there could be a `FloatVec<C>` type that can be used as `FloatVec<4> | FloatVec<5>` etc. That seems to me like the best way to support it compiler side instead of passing that work on to the standard library like I believe C# does.  
> **username_7** (2023-08-29): These days, shader `float3`/etc types are not typically compiled to SIMD operations in the way you might expect. Instead, shader compilers exploit the fact that the program is written in terms of a single vertex/fragment/etc but intended to run on a large number of them independently. The SIMD lanes are used to parallelize across those, rather than anything within the source program.

This is typically referred to as "SPMD."  
---
### Answer by username_8 on 2023-05-16. Score: 6
Probably the most portable way to do this is to leverage the very sophisticated optimisations, including automatic vectorisation, provided by established compiler backends such as LLVM. For the vast majority of language implementors, compiling to a suitable IR and then letting a third-party backend perform the low-level optimisations will be a much better trade-off of implementor effort to program performance, than implementing such optimisations oneself, especially if there is a need to support multiple target architectures (i.e. portability).
For example, here's a Rust function which naively adds four floats:
pub struct Vec4 {
    x: f32, y: f32, z: f32, w: f32,
}

impl Vec4 {
    pub fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
            w: self.w + other.w,
        }
    }
}

Rust's compiler uses LLVM as a backend. Here's the output from Godbolt Compiler Explorer, with optimisations enabled:
example::Vec4::add:
        mov     rax, rdi
        movups  xmm0, xmmword ptr [rsi]
        movups  xmm1, xmmword ptr [rdx]
        addps   xmm1, xmm0
        movups  xmmword ptr [rdi], xmm1
        ret

The addition is done by the addps instruction, which:

Adds four, eight or sixteen packed single-precision floating-point values from the first source operand with the second source operand, and stores the packed single-precision floating-point result in the destination operand.

So the programmer didn't need to give any extra context or hints in the source code, the LLVM backend was smart enough to vectorise it anyway.
> **Comments:**
> **username_2** (2023-09-08): I would not necessarily say this is portable, as unfortunately depending on the whims of the optimizer is rather brittle. That is, while LLVM may auto-vectorize this version of the code: it could fail to auto-vectorize the next version, GCC could fail to auto-vectorize this version, and of course it could also fail on some platforms...  
> **username_8** (2023-09-08): @MatthieuM. That's a fair point, but I think for major compiler backends like LLVM and GCC, the failure to vectorise this kind of code (when compiling with suitable optimisation flags enabled) would be considered a bug. I don't have enough experience with either to confirm this, but I'd be surprised if that kind of optimisation wasn't tested for in those projects' automated tests.  
> **username_9** (2023-09-18): I wouldn't regard this as a reliable means of allowing programmers to write efficient code in implementation-agnostic fashion.  If a program would require that a compiler find a particular optimization in order to yield good performance, there should be a means of explicitly inviting that optimization.  In many cases, performing optimizing transforms is very easy compared with identifying cases where they will be worthwhile, and validating that the transforms will not affect behavior *in ways contrary to application requirements*.  
> **username_9** (2023-09-18): If a programming language has a construct which specifies that a range of code should be executed once for each value of `i` within a certain range, but the loop iterations may be performed in any order, and a `break` may skip any, all, or none, of the remaining iterations, a compiler will be able to transform to use vector or parallel operations much more easily, safely, and effectively, than it could parallelize a "for" loop written via in conventional fashion.  
---
### Answer by username_0 on 2023-05-17. Score: 6
Array Programming
One possible way is to have arrays similar to C++ std::valarray or Fortran arrays. Including semantics to perform operations on an entire array would be higher level than the compiler-specific SIMD types but could easily be implemented with SIMD.
An example of array programming semantics in a C-style language:
int array_1[4] = {1, 2, 3, 4};
int array_2[4] = {5, 6, 7, 8};
int array_3 = array_1+array_2; // {6, 8, 10, 12}
// Array-wise operations could be trivially identified and implemented with SIMD instructions
> **Comments:**
> **username_10** (2023-05-17): This "hypothetical language", of course, is older than a lot of people here: Fortran 77.  
---
### Answer by username_11 on 2023-08-24. Score: 6
One important consideration is how your language handles aliasing.  This is a major killer of auto-vectorization opportunities for C and C++.
For example, in the following function:
void add(float *out, const float *a, const float *b, size_t count) {
    for (size_t i = 0; i < count; i++) {
        out[i] = a[i] + b[i];
    }
}

the compiler must not emit the obvious vectorized code, because the caller is allowed to pass a pointer for out that overlaps one of the input arrays.  Sometimes they can vectorize by adding code to compare the pointers for overlap, and falling back to a naive scalar implementation if they do.
C99 tried to ameliorate this with restrict, with which you can promise the compiler that the buffers will not overlap.  But its definition involves complicated semantics for when an access occurs indirectly "through" the pointer out.  Moreover, it's up to you, the programmer, to ensure that all callers keep that promise, on pain of undefined behavior, and the compiler can't necessarily detect violations.
On the other hand, in Rust, the equivalent code can be auto-vectorized easily and safely.  You have to borrow the output array in order to pass its reference to add, and this guarantees that no other reference (such as to the input arrays) can alias it.  The compiler can and does ensure that callers keep this promise.
> **Comments:**
> **username_3** (2023-09-08): Good point, but in some cases the input and output arrays are the same, or overlapping. The programmer needs to ensure that the algorithm is correct in both cases, pointers aliasing and not aliasing. The compiler typically does alias analysis and emits different code for the *"may-alias"* and *"cannot-alias"* cases.  
> **username_11** (2023-09-08): @MartinBerger: Yes, that's exactly the point I am making.  
> **username_9** (2023-09-18): @NateEldredge: The C99 rules could have been simple if it recognized a three-way split of objects definitely based on P, objects definitely not based on P, and objects that might be based on P, with aliasing restricted only between the first two, and accepting that not all objects could be classified into the first two categories (though any and all objects could safely be classified into the third).  Worse than the complexity, however, is the fact that the rules even as implemented have weird and bizarre corner cases which result in equality comparisons having side effects.  
> **username_9** (2023-09-18): @MartinBerger: Indeed, many optimizations rely upon a compiler's ability to safely assume that `arr1[i]` will not alias `arr2[i+n]` for a constant non-zero `n`.  Such an assumption might not hold if `arr1` and `arr2` are different and identify overlapping regions of storage, but would hold in both the "pointers equal" and "storage is disjoint" cases, without code needing to distinguish between them.  Worse, the way clang and gcc process `restrict` means that even code that specially handles the "pointers are equal" case may be transformed in breaking fashion.  
---
### Answer by username_2 on 2023-09-08. Score: 3
TL;DR: This is very much an open question.
Challenges
There are two challenges:

The varying size of registers: some CPU only allow specific fixed-sizes (Intel CPUs go 128 bits, 256 bits, 512 bits) while others allow variable-sizes (up to a maximum).
Various CPU families, and various CPUs within a family, have different sets of instructions, and while vector addition is a mainstay and fairly uniform, shuffle or lookup-table instructions are quite disparate.

The problem, therefore, is that given the incredible diversity of the instructions, it is hard to come up with an abstraction.
Partial Handling of Size Differences
Handling the varying sizes of common instructions (vector addition, vector multiplication) is perhaps the simplest task.
Apart from built-in language/compiler features, the use of generics allows the user to express a transformation regardless of the vector size. Combined with a library that handles the selection of the size -- with a mix of compile-time and run-time -- this works fairly well.
As an example consider the faster library (Rust):
let lots_of_3s = (&[-123.456f32; 128][..]).simd_iter()
    .simd_map(f32s(0.0), |v| {
        f32s(9.0) * v.abs().sqrt().rsqrt().ceil().sqrt() - f32s(4.0) - f32s(2.0)
    })
    .scalar_collect();

And note how the users is manipulating f32s without specifying the number of lanes.
This works because the language provides:

Generics.
Compile-time detection of the set of number of lanes potentially available.
Run-time detection (standard library) of the set of number of lanes actually available.
Intrinsics, mapping to the desired SIMD instructions.

Unfortunately, this solution is only partial because it only uses "input-driven" number of lanes picking. If the user wished to avail themselves from table-lookup instructions, then it would be harder to abstract that -- as the very way the table is populated depends on the number of lanes actually available.
In such cases, the user essentially needs to construct multiple code-paths, each for a given number of lanes, or restrict execution to the lowest common denominator. And even then, the user should beware of semantic differences between instructions sets.
Language (& Run-time)
In the end, a power-user will, today, outshine any auto-vectorization... because a power-user can use SIMD instructions in ways that boggle the mind (see simdjson) and no compiler has optimizations that are that sophisticated.
To support a power-user, then a language should provide:

Intrinsics: so that the power-user can directly call the vector instruction.
Compile-time/Run-time detection/selection facilities: so that the power-user can choose which code-path to use based on the current CPU model (and generic CPU family) and possibly refuse to use older models.
> **Comments:**
> **username_3** (2023-09-08): Agreed, this is **very much** not a solved problem. You can also see this from the many ISA extensions the processor designers have been cooking up.  
---
### Answer by username_4 on 2023-08-28. Score: 1
First, you make it part of the language, so it isn't non-portable because some compilers don't support it or use different syntax.

Now lets say processor A has vector registers containing two single precision numbers, while processors B, C and D have vector registers with 4, 8, or 16 single precision numbers. How do you do this portably? You can allow declaring a type "array of 16 single precision numbers" or "array of 17 single precision numbers" everywhere and the compiler produces for example code for 8, 4, 2 or 1 operations, or for one additional operation.

The programmer can look at the size of a register and create different instructions depending on the register size.

And every processor has some instructions that don't fit into any scheme in the language, in that case there could be intrinsic functions that work at differnt speeds depending on the processor.
---
