Usar SIMD e intrínsecos de hardware no .NET

SIMD (instrução única, vários dados) é suporte de hardware para aplicar uma operação a várias partes de dados em paralelo com uma única instrução. O código vetorizado processa vários valores por iteração em vez de um, o que pode aumentar consideravelmente a taxa de transferência para o tipo de trabalho numérico, científico, gráfico, processamento de texto e paralelo de dados em que a mesma operação se repete em um buffer. A contrapartida é a complexidade adicional, então só vale mais a pena quando os dados de entrada são grandes o suficiente e o ganho é confirmado por medições.

.NET fornece vários tipos de suporte a SIMD. Escolha aquele que corresponda à quantidade de controle necessária e à complexidade que você está disposto a assumir.

Tipos de suporte a SIMD no .NET

API Namespace Quando utilizá-lo
Tipos de vetor e matriz de finalidade fixa System.Numerics Gráficos e matemática de geometria com vetores de elemento 2 a 4, matrizes, quatérnios e planos.
Vector<T> System.Numerics Vetorização portátil de largura variável quando você não precisa de controle por plataforma.
Vector64<T>, Vector128<T>, , Vector256<T>Vector512<T> System.Runtime.Intrinsics Vetorização de largura fixa e multiplataforma com controle refinado. Esse é o ponto de partida recomendado para novos algoritmos vetorizados.
Funções intrínsecas de hardware System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, System.Runtime.Intrinsics.Wasm Instruções específicas do processador que as APIs de nível superior não disponibilizam, para extrair o máximo de desempenho em um trecho crítico do código.
TensorPrimitives System.Numerics.Tensors Matemática pronta e vetorizada em intervalos. Ele faz a vetorização para você.

Onde essas APIs se sobrepõem, elas se relacionam por meio de camadas de abstração. Os tipos genéricos de vetor são os tipos fundamentais de intercâmbio que as outras camadas usam, portanto são tecnicamente o nível mais baixo: o de largura variável Vector<T>, que se expande para qualquer largura compatível com o hardware em execução, e os de largura fixa Vector64<T> até Vector512<T>. Os intrínsecos de hardware específicos de plataforma em System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm e System.Runtime.Intrinsics.Wasm operam nesses tipos, cada um correspondendo diretamente a uma instrução individual do processador. As operações multiplataforma expostas nos tipos genéricos ficam uma etapa acima dos intrínsecos específicos da plataforma, reduzindo-as para cada destino. Acima delas estão as APIs gerenciadas que operam sobre buffers completos — métodos vetorizados em Span<T> e string, e TensorPrimitives — que se baseiam nas camadas abaixo, de modo que você obtém aceleração SIMD sem precisar escrever nada disso manualmente. Os System.Numerics tipos de forma fixa são tipos auxiliares específicos do domínio para computação gráfica e geometria e não fazem parte desta pilha de intercâmbio.

O restante deste artigo funciona por meio dessas APIs do nível mais alto para o mais baixo e, em seguida, aborda testes, benchmarking e práticas recomendadas.

Tipos de vetores e matrizes de System.Numerics

O System.Numerics namespace fornece tipos acelerados por SIMD com uma forma fixa:

Esses tipos se adaptam naturalmente a gráficos e geometria, e o ambiente de execução acelera suas operações com instruções SIMD quando houver suporte de hardware. O exemplo a seguir adiciona dois vetores:

Vector2 v1 = Vector2.Create(0.1f, 0.2f);
Vector2 v2 = Vector2.Create(1.1f, 2.2f);
Vector2 sum = v1 + v2;

Eles também expõem a matemática de vetor comum que você esperaria, como produto de ponto, distância e fixação:

float dot = Vector2.Dot(v1, v2);
float distance = Vector2.Distance(v1, v2);
Vector2 clamped = Vector2.Clamp(v1, Vector2.Zero, Vector2.One);

Os tipos de matriz dão suporte à matemática de matriz, como transposição e multiplicação:

Matrix4x4 m1 = Matrix4x4.Create(
    1.1f, 1.2f, 1.3f, 1.4f,
    2.1f, 2.2f, 3.3f, 4.4f,
    3.1f, 3.2f, 3.3f, 3.4f,
    4.1f, 4.2f, 4.3f, 4.4f);

Matrix4x4 m2 = Matrix4x4.Transpose(m1);
Matrix4x4 product = Matrix4x4.Multiply(m1, m2);

Vetor<T>

Vector<T> representa um vetor de largura variável de um tipo numérico primitivo. Seu comprimento é fixo para o tempo de vida do processo, mas o valor depende da Vector<T>.Count CPU que executa o código. O compilador just-In-Time (JIT) trata Count como uma constante, portanto, loops gravados nele otimizam bem.

Vector<T> fornece vetorização portátil sem código por plataforma, ao custo de não saber a largura do vetor no tempo de compilação. O exemplo a seguir calcula a adição em termos de elemento de duas matrizes:

// Illustrative: element-wise add with Vector<T>. In practice, prefer the already-accelerated
// TensorPrimitives.Add, which is optimized for every Vector<T>.IsSupported element type.
public static double[] Add(double[] left, double[] right)
{
    ArgumentNullException.ThrowIfNull(left);
    ArgumentNullException.ThrowIfNull(right);
    ArgumentOutOfRangeException.ThrowIfNotEqual(right.Length, left.Length);

    double[] result = new double[left.Length];

    int i = 0;

    // Vector<T>.Count is a JIT-time constant, so the compiler optimizes the loop bound.
    int lastVectorStart = left.Length - Vector<double>.Count;

    for (; i <= lastVectorStart; i += Vector<double>.Count)
    {
        Vector<double> v1 = Vector.Create(left.AsSpan(i));
        Vector<double> v2 = Vector.Create(right.AsSpan(i));
        (v1 + v2).CopyTo(result, i);
    }

    // Process any remaining elements that don't fill a full vector.
    // Simplified for illustration: a scalar tail isn't optimal. A vectorized
    // remainder that reprocesses the last full vector avoids the per-element loop.
    for (; i < left.Length; i++)
    {
        result[i] = left[i] + right[i];
    }

    return result;
}

Observação

Este exemplo é ilustrativo. Você raramente precisa escrever um loop como este manualmente, porque TensorPrimitives já fornece matemática acelerada baseada em extensão. Essa adição em termos de elemento é Add, e reduções como Sum são fornecidas também. Essas operações são aceleradas por hardware para os tipos de elementos aos quais Vector<T> oferece suporte (Vector<T>.IsSupported).

Verificar se há aceleração de hardware

Os tipos acelerados por SIMD funcionam mesmo em configurações de hardware ou JIT que não dão suporte ao SIMD, pois retornam às implementações de software não aceleradas. Para verificar se a aceleração está de fato disponível, verifique a propriedade relevante IsHardwareAccelerated:

Essas propriedades são transformadas em constantes pelo JIT, portanto, as ramificações que você não usa são eliminadas e não há nenhum custo de runtime para verificá-las. Não armazene os valores em cache; leia-os diretamente onde você precisa deles. O mesmo se aplica às Count propriedades (por exemplo, Vector128<T>.Count), que também são constantes JIT-time.

A maioria das operações em uma determinada largura acelerada também é acelerada, mas isso não é garantido para todas as operações. Por exemplo, a divisão em ponto flutuante pode ser acelerada, enquanto a divisão inteira não pode. Quando Vector256 tem aceleração, Vector128 geralmente também tem, mas não há garantia; portanto, verifique cada largura usada.

Tip

Se uma operação necessária não for acelerada em uma plataforma com a qual você se importa ou quiser uma nova API multiplataforma, registre um problema no dotnet/runtime. O mesmo se aplica a melhorias de codegen.

Nem todo tipo de elemento é válido para cada vetor. Vector128<T>e seus irmãos dão suporte aos tipos numéricos primitivos (byte, sbyte, , short, ushort, int, uintlong, ulong, float, , doublee nintnuint) hoje, e esse conjunto pode crescer para incluir outros tipos no futuro. Use Vector128<T>.IsSupported para determinar se um determinado T é válido, o que é especialmente útil em código genérico.

Tipos que não têm suporte, como char e bool, ainda podem ser vetorizados reinterpretando o buffer como um tipo com suporte do mesmo tamanho. Use Cast para reinterpretar um span — por exemplo, de char para ushort — ou o método As<TFrom, TTo> do vetor para reinterpretar um vetor que você já possui. A reinterpretação só altera o tipo, não os bits subjacentes, portanto é sua responsabilidade manter os dados bem formados: um bool deve continuar sendo 0 ou 1, e um char deve permanecer uma unidade de código UTF-16 válida. Se uma operação vetorizada puder produzir um valor fora do intervalo, tome cuidado para normalizar o resultado antes de escrevê-lo novamente.

Vetorização entre plataformas com o Vector128

Vector128<T> é o denominador comum em todas as plataformas que dão suporte à vetorização, portanto, é o melhor lugar para começar. Ele contém um vetor de 128 bits: 16 bytes, 8 shorts, 4 ints/floats ou 2 longs/doubles.

------------------------------128-bits---------------------------
|             64                |               64              |
-----------------------------------------------------------------
|      32       |      32       |      32       |      32       |
-----------------------------------------------------------------
|  16   |  16   |  16   |  16   |  16   |  16   |  16   |  16   |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------

Vector256<T> é duas vezes mais largo e Vector512<T> duas vezes mais largo. Nem todo hardware dá suporte a larguras maiores, por isso os exemplos a seguir usam Vector128 por questões de portabilidade.

Cada largura tem um tipo genérico (Vector128<T>) para os dados e uma classe estática não genérica (Vector128) que contém a maioria das operações, incluindo métodos de fábrica estáticos como Create e Load. Operadores como +, & e << são a maneira idiomática de expressar operações aritméticas e de bits; prefira-os aos métodos nomeados equivalentes para evitar bugs de precedência de operadores e melhorar a legibilidade. Para algoritmos que dependem da ordem dos bytes, faça a ramificação com base em IsLittleEndian, que o JIT também reduz a uma constante.

Observação

No x86/x64, Vector256<T> as operações geralmente são tratadas como duas "lanes" independentes de 128 bits. Para a maioria das operações elemento a elemento, isso é transparente, mas operações que cruzam vias (como reorganizações ou operações aos pares/horizontais) podem se comportar de forma diferente ou ter um custo maior do que o equivalente Vector128. Confirme com parâmetros de comparação antes de assumir que um vetor mais amplo é mais rápido.

As operações de travessia de pista não são ampliadas gratuitamente

As operações em termos de elemento não se importam com a largura: v1 + v2 é o mesmo resultado por elemento se v1 e v2 são Vector128<T> ou Vector256<T>— a ampliação apenas processa mais uma faixa de dados por instrução. Add em v = [a, b, c, d] e w = [e, f, g, h] sempre combina elementos de mesmo índice:

v: [ a | b | c | d ]
w: [ e | f | g | h ]
     +   +   +   +
r: [a+e|b+f|c+g|d+h]

As operações de passagem de faixa não são dimensionadas de forma simples, pois quais elementos são combinados dependem da largura do vetor. Uma redução par a par combina elementos adjacentes em vez de elementos com o mesmo índice, portanto, a ampliação muda quais elementos acabam sendo emparelhados:

v:       [  a  |  b  |  c  |  d  ]
            \_____/     \_____/
round 1: [ a+b | c+d | a+b | c+d ]
            \_________________/
round 2: [  S  |  S  |  S  |  S  ]   (S = a+b+c+d)

Isso é exatamente o que uma redução horizontal faz: somar os elementos de um vetor com duas rodadas de adição par. Em x86/x64, Vector128<float> (4 elementos) permite chegar lá com duas chamadas para HorizontalAdd:

// Sums all four elements with two rounds of pairwise horizontal adds.
// HorizontalAdd(v, v) on [a, b, c, d] gives [a+b, c+d, a+b, c+d]; a second round
// collapses that to the full sum in every element.
public static float SumVector128(Vector128<float> v)
{
    Debug.Assert(Sse3.IsSupported);

    Vector128<float> step1 = Sse3.HorizontalAdd(v, v);
    Vector128<float> step2 = Sse3.HorizontalAdd(step1, step1);

    return step2.ToScalar();
}

Estenda o mesmo padrão de duas chamadas para Vector256<float> (8 elementos) e parece correto — mas não está. HorizontalAdd não opera em todo o vetor de 256 bits; ele repete o padrão par de forma independente dentro de cada faixa de 128 bits. Duas iterações produzem a soma da metade inferior (elementos 0-3), replicada por toda a metade inferior, e a soma da metade superior (elementos 4-7), replicada por toda a metade superior, não a soma dos oito elementos:

// The same two-round pattern on Vector256<float> looks like it should sum all eight
// elements, but Avx.HorizontalAdd repeats the pairwise pattern independently within
// each 128-bit lane. The result holds the lower lane's sum (elements 0-3) broadcast
// across the lower lane and the upper lane's sum (elements 4-7) broadcast across the
// upper lane -- ToScalar only returns the lower lane's partial sum, not the total.
public static float SumVector256Naive(Vector256<float> v)
{
    Debug.Assert(Avx.IsSupported);

    Vector256<float> step1 = Avx.HorizontalAdd(v, v);
    Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

    return step2.ToScalar();
}

Para obter o total correto, faça a ponte entre o limite da pista explicitamente: leia a soma parcial de cada faixa e GetLower/GetUpper adicione-as eGetLowerGetUpper divida um vetor em suas primeiras e segundas metades:

------------------------------128-bits---------------------------
|           LOWER               |             UPPER             |
-----------------------------------------------------------------
|      32       |      32       |      32       |      32       |
-----------------------------------------------------------------
|  16   |  16   |  16   |  16   |  16   |  16   |  16   |  16   |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------
// Getting the full sum needs an explicit step to cross the lane boundary: read each
// lane's partial sum out with GetLower/GetUpper and add them together.
public static float SumVector256(Vector256<float> v)
{
    Debug.Assert(Avx.IsSupported);

    Vector256<float> step1 = Avx.HorizontalAdd(v, v);
    Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

    Vector128<float> lower = step2.GetLower();
    Vector128<float> upper = step2.GetUpper();

    return lower.ToScalar() + upper.ToScalar();
}

Essa etapa extra é o custo real das pistas de travessia. Um algoritmo com cruzamento entre pistas não se amplia gratuitamente como um algoritmo elemento a elemento — meça antes de presumir que o vetor mais largo é melhor.

Operações comuns

Vector128 e suas variantes maiores expõem uma grande superfície da API. Você não precisa memorizá-lo , conhecer as categorias e pesquisar os detalhes quando precisar delas. Cada operação tem uma implementação alternativa em software para plataformas que não conseguem acelerá-la. A tabela a seguir abrange essencialmente toda a superfície.

Categoria O que faz APIs representativas
Constants Vetores constantes predefinidos Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero
Criação Transmitir um escalar, definir elementos ou gerar uma sequência Create, CreateScalar, CreateScalarUnsafe, Create(ReadOnlySpan<T>), CreateSequence, CreateGeometricSequence, CreateHarmonicSequence, , CreateAlternatingSequence
Carregar e armazenar Mover dados entre a memória e um vetor Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, Store, , StoreUnsafe, StoreAligned, , StoreAlignedNonTemporal, CopyTo, TryCopyTo
Arithmetic Matemática e reduções em termos de elementos Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum
Operações de bits Lógica bit a bit e deslocamentos BitwiseAnd (x & y), BitwiseOr (x \| y), Xor (x ^ y), AndNot (x & ~y), OnesComplement (~x), ShiftLeft (x << n), ShiftRightArithmetic (x >> n), , ShiftRightLogical (x >>> n)
Min, max e clamp Limitação por elemento de mínimo, máximo e intervalo Min, Max, Clamp, MinMagnitude, , MaxMagnitude, MinNumber, MaxNumber, , MinMagnitudeNumberMaxMagnitudeNumber
Arredondamento Arredondar cada elemento para um número inteiro Ceiling, Floor, , RoundTruncate
Funções matemáticas Funções auxiliares de sinal, interpolação, ângulo e funções transcendentais CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2
Comparison Compare cada elemento; o resultado é uma máscara vetorial, não o que um operador bool fornece Equals, GreaterThan, GreaterThanOrEqual, , LessThanLessThanOrEqual
Classification Predicados por elemento sobre a linha numérica, cada um retornando uma máscara de vetor IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero
Reduções por comparação Reduzir uma comparação por elemento a um único bool EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAll, , GreaterThanOrEqualAny, LessThanAll, , LessThanAny, LessThanOrEqualAll, LessThanOrEqualAny
Predicados de vetor completo Reduzir um vetor a um bool: indicando se todos, algum ou nenhum elemento é igual a um valor ou se (as formas WhereAllBitsSet) têm todos os bits ativados. Prefira estes em vez de converter uma máscara em um índice All, Any, None, AllWhereAllBitsSet, , AnyWhereAllBitsSetNoneWhereAllBitsSet
Pesquisa Contar ou localizar elementos pelo valor ou (as formas WhereAllBitsSet) definir pistas em uma máscara Count, IndexOf, LastIndexOf, CountWhereAllBitsSet, , IndexOfWhereAllBitsSetLastIndexOfWhereAllBitsSet
Máscara para indexação Transformar uma máscara de comparação em uma máscara de bits escalar e examiná-la ExtractMostSignificantBits com TrailingZeroCount ou LeadingZeroCount
Seleção Combinar dois vetores de acordo com uma máscara, bit a bit ConditionalSelect(x, y, z), equivalente a (y & x) \| (z & ~x)
Conversion Alterar o tipo numérico, computando novos valores (por exemplo, int para float) ConvertToInt32, ConvertToInt64, ConvertToUInt32, ConvertToUInt64, , ConvertToSingleConvertToDouble
Ampliação e estreitamento Dividir elementos em um tipo mais amplo ou compactá-los em um tipo mais estreito Widen, WidenLower, WidenUpper, , NarrowNarrowWithSaturation
Reinterpretação Reinterpretar os bits como outro tipo de elemento sem alterá-los As<TFrom, TTo>, AsByte, AsInt32, AsSinglee as outras As* formas de elemento
Interoperabilidade com System.Numerics Reinterpretar entre Vector128<T> e os tipos numéricos de formato fixo AsVector, AsVector2, AsVector3, AsVector4, AsPlane, AsQuaternion, AsVector128, , AsVector128Unsafe
Reordenar Reorganizar elementos por índice ou intercalar dois vetores Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper
Acesso à faixa de tráfego Leia ou substitua elementos e metades individuais, ou redimensione o vetor GetElement, WithElement, ToScalar, GetLower, GetUpper, WithLower, WithUpper, , ToVector256

Tip

Várias operações têm variantes Estimate e Native — por exemplo, MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative e ConvertToInt32Native. Elas são mapeadas para uma instrução de hardware mais rápida que sacrifica um pouco de precisão ou abre mão de uma garantia do IEEE para casos extremos (como o tratamento de NaN), portanto recorra a elas apenas quando um benchmark mostrar que a forma exata é o gargalo e a semântica menos estrita for aceitável.

Observação

Vector256.Shuffle trata sua entrada como um único vetor de 256 bits, enquanto o Avx2.Shuffle específico da plataforma opera como duas vias independentes de 128 bits. A API multiplataforma é a opção mais portátil, mas confirme o comportamento de que você precisa ao portar intrínsecos implementados manualmente.

Estruture o caminho do código

Um método vetorizado normalmente se desdobra em um caminho para cada largura de vetor, mais um fallback escalar para entradas pequenas e hardware sem aceleração. Para usar o maior vetor compatível com o hardware, verifique o vetor mais amplo primeiro e trabalhe para baixo:

// Sums a buffer, choosing the widest vector the hardware and element type support.
public static T Sum<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    // The widest-first order continues with the Vector512 and Vector256 paths, which belong
    // here ahead of the Vector128 block below. They're identical to it aside from the wider
    // type (for example, Vector512<T> with Vector512.Create and Vector512.Sum), so they're
    // omitted for brevity:
    //
    // if (Vector512.IsHardwareAccelerated && Vector512<T>.IsSupported)
    // {
    //     if (buffer.Length >= Vector512<T>.Count)
    //     {
    //         return SumVector512(buffer);
    //     }
    //     return SumVectorSmall(buffer);
    // }
    //
    // if (Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported)
    // {
    //     if (buffer.Length >= Vector256<T>.Count)
    //     {
    //         return SumVector256(buffer);
    //     }
    //     return SumVectorSmall(buffer);
    // }

    if (Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported)
    {
        if (buffer.Length >= Vector128<T>.Count)
        {
            return SumVector128(buffer);
        }
        return SumVectorSmall(buffer);
    }

    return SumScalar(buffer);
}

A proteção externa de cada largura combina Vector128.IsHardwareAccelerated (uma constante em tempo de JIT que indica se a plataforma acelera essa largura) com Vector128<T>.IsSupported (se o tipo de elemento T é válido para essa largura). Dentro de um bloco compatível, compare o comprimento da entrada com Count para escolher entre o caminho vetorizado e uma alternativa para entradas pequenas. O método é genérico em relação a T, e os blocos Vector256 e Vector512 — idênticos ao bloco Vector128, mas usando o tipo mais amplo — são mostrados comentados, por brevidade.

Há duas alternativas de contingência distintas. Um buffer muito pequeno até mesmo para o vetor mais estreito, mas em hardware acelerado, vai para SumVectorSmall— uma tabela de salto explícita switch que manipula cada comprimento de sub-vetor possível sem um loop:

// Sums a buffer smaller than the widest vector. The complete "optimal" shape dispatches on the
// element width so each width uses a switch jump table sized to the number of elements that fit
// in the widest vector (63 for byte, 31 for short, 15 for int/float, 7 for long/double).
private static T SumVectorSmall<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    // sizeof(T) is a JIT constant, so only the matching branch survives for a given T.
    if (sizeof(T) == 4)
    {
        return SumVectorSmall4(buffer);
    }

    // The 1-, 2-, and 8-byte tables share the shape below, sized for their element width.
    // They're omitted for brevity, so those widths fall back to a scalar loop here:
    //
    // if (sizeof(T) == 1) return SumVectorSmall1(buffer); // switch over lengths 0..63
    // if (sizeof(T) == 2) return SumVectorSmall2(buffer); // switch over lengths 0..31
    // if (sizeof(T) == 8) return SumVectorSmall8(buffer); // switch over lengths 0..7
    return SumScalar(buffer);
}

private static T SumVectorSmall4<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);
    Debug.Assert(buffer.Length < Vector512<T>.Count);

    T result = T.Zero;

    // A 4-byte element gives Count == 4/8/16 for Vector128/256/512, so a remainder can be up to
    // 15 elements. The larger cases fold the leftover with the widest vector that fits, using two
    // overlapping loads (one from the start, one from the end) rather than recursing—the shape
    // TensorPrimitives uses. The loads overlap for lengths that aren't an exact multiple of the
    // width, so the tail is masked down to the additive identity before it's summed. That mask is
    // only needed because addition is non-idempotent; an idempotent operation such as a search
    // could fold the overlapping tail in directly.
    switch (buffer.Length)
    {
        // One or two Vector256's worth of data.
        case 15:
        case 14:
        case 13:
        case 12:
        case 11:
        case 10:
        case 9:
        case 8:
        {
            Vector256<T> beg = Vector256.Create(buffer);
            Vector256<T> end = Vector256.Create(buffer.Slice(buffer.Length - Vector256<T>.Count));

            Vector256<T> msk = CreateRemainderMask256<T>(buffer.Length - Vector256<T>.Count);
            end = Vector256.ConditionalSelect(msk, end, Vector256<T>.Zero);

            result = Vector256.Sum(beg + end);
            break;
        }

        // One or two Vector128's worth of data.
        case 7:
        case 6:
        case 5:
        case 4:
        {
            Vector128<T> beg = Vector128.Create(buffer);
            Vector128<T> end = Vector128.Create(buffer.Slice(buffer.Length - Vector128<T>.Count));

            Vector128<T> msk = CreateRemainderMask128<T>(buffer.Length - Vector128<T>.Count);
            end = Vector128.ConditionalSelect(msk, end, Vector128<T>.Zero);

            result = Vector128.Sum(beg + end);
            break;
        }

        // Smaller than a single vector: each case falls through to the next, accumulating one
        // element per label.
        case 3:
        {
            result += buffer[2];
            goto case 2;
        }

        case 2:
        {
            result += buffer[1];
            goto case 1;
        }

        case 1:
        {
            result += buffer[0];
            goto case 0;
        }

        case 0:
        {
            break;
        }
    }

    return result;
}

// Builds a mask whose last `keepLast` lanes are all-bits-set and the rest zero, so an overlapping
// tail load can be folded in without double-counting the lanes the head already covered.
// TensorPrimitives uses an internal table-based helper. The mask is only a bit pattern keyed on
// lane width, so it's built with the same-width integer Indices ([0, 1, 2, ...]) and reinterpreted
// to T: integer comparisons are cheaper than floating-point ones, so a float/double table would
// still compare as int/long rather than in its own element type.
private static Vector256<T> CreateRemainderMask256<T>(int keepLast)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);

    Vector256<int> firstKept = Vector256.Create(Vector256<int>.Count - keepLast);
    return Vector256.GreaterThanOrEqual(Vector256<int>.Indices, firstKept).As<int, T>();
}

private static Vector128<T> CreateRemainderMask128<T>(int keepLast)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);

    Vector128<int> firstKept = Vector128.Create(Vector128<int>.Count - keepLast);
    return Vector128.GreaterThanOrEqual(Vector128<int>.Indices, firstKept).As<int, T>();
}

sizeof(T) também é uma constante em tempo de JIT, portanto SumVectorSmall faz o despacho com base na largura do elemento para uma tabela dimensionada para comportar a quantidade de elementos do vetor mais largo — a mesma abordagem usada por TensorPrimitives. (Quando o novo modelo de segurança de memória está habilitado, sizeof(T) expressões em um parâmetro de tipo com a unmanaged restrição são permitidas em código seguro.) Somente a tabela de 4 bytes é mostrada; as tabelas de 1 byte, 2 bytes e 8 bytes compartilham sua forma. Seus casos de maior tamanho tratam o restante remanescente com um(a) Vector256 ou Vector128, usando duas cargas sobrepostas — uma a partir do início e outra a partir do fim —, de modo que o tratamento mais amplo do restante para os caminhos Vector512/Vector256 omitidos fica diretamente na tabela de salto. As duas cargas se sobrepõem sempre que o comprimento não é um múltiplo exato da largura, portanto, a cauda é mascarada para a identidade aditiva com ConditionalSelect antes de ser resumida. Essa máscara só é necessária porque a adição não é idempotente; uma operação idempotente, como uma pesquisa, pode dobrar a cauda sobreposta diretamente. Um buffer em hardware sem qualquer vetorização recai em SumScalar, um loop escalar comum.

Faça loop sobre a entrada e manipule o restante

Para processar um buffer maior que um único vetor, faça loop sobre ele um vetor de cada vez e, em seguida, manipule os elementos restantes que não preenchem um vetor completo. A maneira robusta de lidar com essa cauda é reprocessar a quantidade de elementos correspondente ao último vetor completo, sobrepondo alguns elementos que o loop já processou — o que evita um epílogo escalar separado. Se essa sobreposição precisa ser corrigida depende da operação.

Uma operação não idempotente, como uma soma, contaria os elementos sobrepostos duas vezes; portanto, mascare esses elementos para o valor identidade da operação antes de reduzi-los. Use isso quando cada elemento precisar contribuir exatamente uma vez:

// Sums a buffer with an unrolled vector loop plus a masked, jump-table remainder.
private static T SumVector128<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported);
    Debug.Assert(buffer.Length >= Vector128<T>.Count);

    // Preload the last full vector, overlapping the tail. Any sub-vector remainder is folded in
    // from here (masked) by case 0 of the switch below, so the loop never falls out to a separate
    // scalar tail—the same shape TensorPrimitives uses.
    Vector128<T> end = Vector128.Create(buffer.Slice(buffer.Length - Vector128<T>.Count));

    // A production implementation would also align the buffer to a vector boundary and, for
    // very large inputs, use non-temporal loads/stores so the data doesn't evict useful
    // cache lines. Both are omitted here; see TensorPrimitives for a complete treatment.

    Vector128<T> sum = Vector128<T>.Zero;

    // Only pay for the four independent accumulators when there's enough data to unroll;
    // smaller payloads skip straight to the remainder below. Four vectors per iteration lets
    // the accumulators pipeline; Vector128.Create reads the first Vector128<T>.Count elements.
    if (buffer.Length >= Vector128<T>.Count * 4)
    {
        Vector128<T> sum0 = Vector128<T>.Zero;
        Vector128<T> sum1 = Vector128<T>.Zero;
        Vector128<T> sum2 = Vector128<T>.Zero;
        Vector128<T> sum3 = Vector128<T>.Zero;

        do
        {
            sum0 += Vector128.Create(buffer);
            sum1 += Vector128.Create(buffer.Slice(Vector128<T>.Count));
            sum2 += Vector128.Create(buffer.Slice(Vector128<T>.Count * 2));
            sum3 += Vector128.Create(buffer.Slice(Vector128<T>.Count * 3));

            buffer = buffer.Slice(Vector128<T>.Count * 4);
        }
        while (buffer.Length >= Vector128<T>.Count * 4);

        // Combine pairwise so the two independent adds can pipeline.
        sum = (sum0 + sum1) + (sum2 + sum3);
    }

    // Split the remainder into its full vectors and a sub-vector tail. The full vectors fall
    // through the jump table; the tail lands in case 0, where the preloaded end is masked so only
    // the trailing elements the full vectors didn't already cover are added.
    (int blocks, int trailing) = Math.DivRem(buffer.Length, Vector128<T>.Count);

    switch (blocks)
    {
        case 3:
        {
            sum += Vector128.Create(buffer.Slice(Vector128<T>.Count * 2));
            goto case 2;
        }

        case 2:
        {
            sum += Vector128.Create(buffer.Slice(Vector128<T>.Count));
            goto case 1;
        }

        case 1:
        {
            sum += Vector128.Create(buffer);
            goto case 0;
        }

        case 0:
        {
            Vector128<T> msk = CreateRemainderMask128<T>(trailing);
            sum += Vector128.ConditionalSelect(msk, end, Vector128<T>.Zero);
            break;
        }
    }

    // Horizontally add the lanes into a single scalar.
    return Vector128.Sum(sum);
}

Esta versão protege o loop desenrolado com um if, para que cargas úteis pequenas ignorem completamente os quatro acumuladores e vão diretamente para o restante. Quando há dados suficientes, um do/while acumula quatro vetores por iteração em acumuladores independentes, o que permite ao pipeline do processador as adições, e os combina de forma emparelhada. Uma switch tabela de salto incorpora então os zero a três vetores completos restantes e, em case 0, a cauda do subvetor: ela reutiliza um vetor completo pré-carregado do fim do buffer, sobrepondo os elementos já processados, e mascara essa sobreposição até a identidade aditiva com ConditionalSelect para que a cauda permaneça vetorizada, em vez de cair em um loop escalar. Como antes, Vector128.CreateVector128<T>.Count elementos do intervalo. O JIT omite a verificação de limites do span para padrões de acesso típicos, portanto Create é uma boa opção padrão mesmo em um loop crítico; LoadUnsafe (abordado a seguir) é a alternativa de nível inferior para quando você percorre o buffer usando uma referência gerenciada. Para entradas muito grandes, uma implementação completa também alinharia o buffer e usaria cargas e armazenamentos não temporais para evitar expulsar linhas de cache úteis — ambos omitidos aqui e abordados em detalhes por TensorPrimitives.

Uma operação idempotente, como buscar um valor, pode reprocessar a sobreposição sem problemas, de modo que incorpora diretamente o último vetor, sem máscara:

// Idempotent search that re-processes the final vector instead of a scalar loop.
public static bool Contains(ReadOnlySpan<int> buffer, int searched)
{
    Debug.Assert(Vector128.IsHardwareAccelerated);

    Vector128<int> values = Vector128.Create(searched);
    ReadOnlySpan<int> remaining = buffer;

    while (remaining.Length >= Vector128<int>.Count)
    {
        if (Vector128.EqualsAny(Vector128.Create(remaining), values))
        {
            return true;
        }
        remaining = remaining.Slice(Vector128<int>.Count);
    }

    if (remaining.IsEmpty)
    {
        return false;
    }

    // A partial vector remains. When the buffer holds at least one full vector,
    // re-check the last one (overlapping the tail); otherwise scan the few elements directly.
    if (buffer.Length >= Vector128<int>.Count)
    {
        Vector128<int> tail = Vector128.Create(buffer.Slice(buffer.Length - Vector128<int>.Count));
        return Vector128.EqualsAny(tail, values);
    }

    foreach (int value in remaining)
    {
        if (value == searched)
        {
            return true;
        }
    }

    return false;
}

Warning

Tratar o resto incorretamente é uma fonte comum de bugs. Um loop que lê além do final do buffer produz resultados não determinísticos e pode falhar. O conjunto de testes do runtime usa um auxiliar BoundedMemory que coloca uma página sem acesso imediatamente após o buffer, de forma que qualquer leitura fora dos limites gera um AccessViolationException durante os testes. Sempre cubra a lógica restante, incluindo buffers cujo comprimento não é um múltiplo da largura do vetor.

Carregar e armazenar vetores com segurança

Para a maioria dos códigos, Vector128.Create(span) e CopyTo são a maneira mais simples de mover dados entre um intervalo e um vetor, e o JIT os mantém eficientes. Quando você precisar de operações de leitura e gravação de nível inferior — por exemplo, para percorrer um buffer por referência gerenciada — prefira as sobrecargas LoadUnsafe e StoreUnsafe que recebem uma referência gerenciada e um deslocamento de elemento nuint. Ao contrário das sobrecargas baseadas em ponteiros Load/Store, elas não exigem fixar o buffer na memória e, ao contrário da aritmética bruta com referências, não exigem que você avance manualmente um ref. É fácil cometer erros com ambas as alternativas, de modo a introduzir falhas no coletor de lixo ou violações de acesso.

Para que os buffers vazios não sejam lançados, obtenha a referência inicial de GetReference (ou GetArrayDataReference para matrizes) em vez de ref span[0].

Importante

A aritmética de deslocamento usa valores sem sinal nuint. Sempre verifique o comprimento do buffer antes de calcular um deslocamento como buffer.Length - Vector128<int>.Count. Se o buffer for menor que um vetor, essa subtração causará underflow, resultando em um valor enorme, e o loop lerá uma região de memória inválida.

Intrínsecos de hardware específicos da plataforma

Quando uma instrução específica do processador oferece uma vantagem que as APIs portáteis não expõem, recorra aos intrínsecos de hardware em System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm e System.Runtime.Intrinsics.Wasm. Cada classe intrínseca tem uma IsSupported propriedade (também uma constante JIT) para que você possa proteger o caminho especializado e voltar ao código portátil em outro lugar:

// Illustrates per-platform lightup. The portable '(vector & mask) == Zero' below
// already lowers optimally, so prefer it unless a specific instruction measurably wins.
public static bool AllBitsClear(Vector128<byte> vector, Vector128<byte> mask)
{
    if (Sse41.IsSupported)
    {
        // x86/x64: a single ptest instruction.
        return Sse41.TestZ(vector, mask);
    }
    else if (AdvSimd.Arm64.IsSupported)
    {
        // Arm64: AND, then reduce the maximum byte across every lane.
        Vector128<byte> anded = AdvSimd.And(vector, mask);
        return AdvSimd.Arm64.MaxAcross(anded).ToScalar() == 0;
    }
    else if (PackedSimd.IsSupported)
    {
        // WebAssembly: AND, then test whether any lane is non-zero.
        return !PackedSimd.AnyTrue(PackedSimd.And(vector, mask));
    }
    else
    {
        // Portable fallback for any other platform.
        return (vector & mask) == Vector128<byte>.Zero;
    }
}

O método anterior mostra como iluminar caminhos de código por arquitetura quando você quiser, mas é deliberadamente um exemplo simples: você não precisa dele aqui. A expressão portátil (vector & mask) == Vector128<byte>.Zero já é compilada para a instrução ideal em cada plataforma (por exemplo, ptest em x86/x64), portanto faz o mesmo trabalho que as ramificações implementadas manualmente, só que sem a complexidade. Recorra a instruções intrínsecas explícitas somente quando uma instrução específica superar de forma mensurável o que as APIs portáteis geram.

Os intrínsecos de hardware exigem uma implementação separada para cada conjunto de instruções; portanto, trate-os como uma otimização para trechos críticos identificados por medição, em vez de usá-los como padrão. As Vector128/Vector256 APIs já são mais baixas para instruções eficientes em cada plataforma e, na prática, o código sofisticado por instrução nem sempre ganha. Confirme a diferença com um parâmetro de comparação antes de se comprometer com a manutenção extra.

Matemática de nível superior com TensorPrimitives

Se você precisar de matemática vetorizada em intervalos e não quiser escrever os loops por conta própria, TensorPrimitives fornecerá um grande conjunto de operações numéricas — aritméticas, exponenciales e reduções, como produto de ponto e similaridade de cosseno — que já são vetorizadas internamente. Ele está disponível no pacote NuGet System.Numerics.Tensors .

// Computes result = (left * right) + addend over the whole span, vectorized internally.
public static float[] MultiplyAdd(float[] left, float[] right, float[] addend)
{
    float[] result = new float[left.Length];

    TensorPrimitives.Multiply(left, right, result);
    TensorPrimitives.Add(result, addend, result);

    return result;
}

// Higher-level reductions are available too.
public static float CosineSimilarity(float[] left, float[] right) =>
    TensorPrimitives.CosineSimilarity(left, right);

Para cargas de trabalho de IA e numéricas, TensorPrimitives geralmente oferece a maior parte do benefício do SIMD escrito à mão sem nenhuma complexidade.

Testar todos os caminhos de código

Como um método vetorizado tem vários caminhos de código, os testes precisam abranger cada um deles: o Vector256 caminho, o Vector128 caminho e o caminho escalar, cada um com entradas grandes o suficiente e pequenas demais para serem beneficiadas. Você pode variar o tamanho de entrada em testes, mas não pode alternar a aceleração de hardware no nível de teste. Em vez disso, controle-o com variáveis de ambiente antes do início do processo:

  • Defina DOTNET_EnableAVX2=0 para fazer com que Vector256.IsHardwareAccelerated retorne false.
  • Defina DOTNET_EnableHWIntrinsic=0 para desabilitar totalmente os intrínsecos, portanto Vector128, Vector64e Vector<T> todos relatam nenhuma aceleração.

Para exercitar cada caminho em um único computador, execute o conjunto de testes uma vez sem substituições, uma vez com DOTNET_EnableAVX2=0, e uma vez com DOTNET_EnableHWIntrinsic=0. A alternativa é executar em uma variedade suficiente de hardware para cobrir todos eles.

Botões de configuração do conjunto de instruções

Além desses dois, o runtime reconhece um botão por agrupamento lógico de conjuntos de instruções, cada um prefixado com DOTNET_. Um único botão pode abranger vários conjuntos de instruções relacionados—EnableAVX2 por exemplo, portões AVX2 juntamente com BMI1, BMI2, F16C, FMA, LZCNT e MOVBE. Ajustar um controle para 0 desabilita todo o seu grupo e tudo o que estiver em camadas sobre ele. Defini-lo como 1 (o padrão para a maioria) habilita o grupo, mas o hardware ainda precisa realmente oferecer suporte a ele — habilitar uma opção que a CPU atual não possui é ignorado, portanto você só pode restringir o que é usado, nunca forçar a ativação de uma instrução sem suporte e acabar causando problemas para si mesmo. DOTNET_EnableHWIntrinsic=0 é a medida mais drástica — desativa tudo até a base, de modo que Vector128, Vector64 e Vector<T> passam a indicar ausência de aceleração, e o código recorre ao caminho de software.

Importante

Estas são ferramentas de diagnóstico, destinadas principalmente a testes e validação — percorrendo todos os caminhos do código, reproduzindo um problema específico de hardware ou confirmando o uso de um mecanismo de contingência. Eles não são projetados para uso geral ou de produção e não são um contrato de estabilidade. O conjunto a seguir é o que o .NET 11 reconhece; versões anteriores apresentavam um conjunto diferente — os controles de base e AVX-512, em particular, foram reconfigurados —, portanto, confirme os nomes em relação à versão do runtime de destino.

Essas ferramentas também têm limites para o que elas alcançam. Como eles determinam as decisões de JIT, não afetam o código que já foi compilado previamente por meio de ReadyToRun ou Native AOT, e não necessariamente afetam as rotinas internas que o runtime e as bibliotecas principais usam. Trate-os como uma maneira de orientar seu próprio código compilado por JIT, não um botão de desativação global para um conjunto de instruções.

O switch base e os limites de largura se aplicam a todas as arquiteturas:

Botão giratório (DOTNET_ prefixo) Default Effect
EnableHWIntrinsic 1 Opção mestra para todos os intrínsecos de hardware; 0 força o caminho de software completo.
MaxVectorTBitWidth padrão do sistema Limita Vector<T> a uma largura máxima em bits; um valor abaixo de 128 indica o padrão do sistema.
PreferredVectorBitWidth padrão do sistema Limita o vetor de largura fixa máxima que relata IsHardwareAccelerated, em bits; um valor abaixo de 128 significa o padrão do sistema.

O padrão do sistema para MaxVectorTBitWidth pode ser mais estreito do que o hardware suporta totalmente, portanto Vector<T> não se expande automaticamente para o vetor mais amplo disponível. Por exemplo, Vector512<T>.IsHardwareAccelerated pode ser true, enquanto Vector<T> continua com 256 bits; defina DOTNET_MaxVectorTBitWidth=512 para que Vector<T> use a largura maior.

PreferredVectorBitWidth define o limite da largura máxima do vetor informada por IsHardwareAccelerated. Reduzi-lo para um valor abaixo do suportado pelo hardware desativa as larguras maiores: em uma máquina que suporta vetores de 512 bits, DOTNET_PreferredVectorBitWidth=256 faz com que Vector512<T>.IsHardwareAccelerated informe false. É um botão geral, mas apenas x86/x64 oferece larguras acima de 128 hoje, então esse é o único lugar em que tem um efeito observável.

Cada agrupamento lógico de conjuntos de instruções x86/x64 também possui sua própria opção:

Botão (prefixo DOTNET_) Default Gates
EnableAVX 1 AVX e dependentes
EnableAVX2 1 AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE e dependentes
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL e dependentes
EnableAVX512BMM 1 AVX-512 BMM
EnableAVX512v2 1 AVX-512 IFMA+VBMI
EnableAVX512v3 1 AVX-512 BITALG+VBMI2+VPOPCNTDQ+VNNI
EnableAVX10v1 1 AVX10.1
EnableAVX10v2 0 AVX10.2
EnableAPX 0 APX (registros de uso geral estendidos)
EnableAES 1 AES, PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 e AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES, VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 SERIALIZAR X86

No Arm64, cada agrupamento lógico de conjuntos de instruções tem sua própria opção:

Knob (prefixo DOTNET_) Default Gates
EnableArm64Aes 1 AES
EnableArm64Atomics 1 Atômicos de extensões grandes do sistema (LSE)
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA zeragem do cache
EnableArm64Dp 1 Produto Dot
EnableArm64Rdm 1 Arredondando o acúmulo de multiplicação de duplicação (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 RCpc (ordenação consistente com processador e consistente com a versão)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 CSSC (compactação de sequência curta comum)
EnableArm64Sve 1 Extensão de vetor escalonável (SVE)
EnableArm64Sve2 1 SVE2
EnableArm64Sha3 1 SHA3
EnableArm64Sm4 1 SM4
EnableArm64SveAes 1 SVE AES
EnableArm64SveSha3 1 SVE SHA3
EnableArm64SveSm4 1 SVE SM4

Uma opção com valor padrão 0 (por exemplo, EnableAVX10v2 ou EnableArm64Cssc) controla a ativação de um conjunto de instruções que ainda está sendo disponibilizado, por isso ele permanece desativado até que você opte por ativá-lo.

Teste de benchmark para confirmar o êxito

A vetorização adiciona complexidade, portanto verifique se ela vale a pena antes de mantê-la. Use BenchmarkDotNet e as mesmas variáveis de ambiente mostradas anteriormente para comparar as implementações escalar, Vector128 e Vector256 em uma única execução. O diagnosticador de desmontagem do BenchmarkDotNet também pode emitir o assembly gerado, o que é inestimável ao ajustar o código de alto desempenho.

Alguns pontos para se lembrar:

  • Entradas de dados maiores se beneficiam mais. Para buffers pequenos, o código vetorizado pode ser mais lento do que o código escalar devido à sobrecarga de instalação. Faça benchmark dos tamanhos de entrada que os chamadores realmente usam.
  • As acelerações raramente são perfeitas. Um vetor de 256 bits operando sobre elementos de 32 bits não será necessariamente 8 vezes mais rápido; a taxa de transferência da memória, o alinhamento e a latência das instruções influenciam esse resultado.
  • O alinhamento de memória afeta a estabilidade. O alinhamento de alocação randomizada adiciona ruído entre execuções. Você pode alocar memória alinhada com AlignedAlloc para obter resultados estáveis, ou habilitar a randomização de memória do BenchmarkDotNet para observar a distribuição completa.

Práticas recomendadas

  • Alcance primeiro as APIs de nível superior existentes. Span<T>, string, LINQ, TensorPrimitives, e os tipos de tensor já aceleram muitas operações comuns para você — não implemente manualmente o que já está otimizado e testado.
  • Comece com Vector128<T>; ele tem aceleração na mais ampla gama de hardware, e você não precisa de Vector256<T> para obter uma implementação correta e portável. Adicione larguras maiores e intrínsecos de hardware apenas aos trechos críticos medidos.
  • Verifique IsHardwareAccelerated e Count diretamente em vez de colocá-los em cache; o JIT os transforma em constantes.
  • Sempre manipule o restante do loop e contabilize a sobreposição de buffers de origem e de destino ao armazenar.
  • Escreva primeiro testes para casos extremos, depois uma solução escalar e então expresse essa lógica escalar usando as APIs vetoriais.
  • Teste cada caminho de código (incluindo violações de acesso) e avalie o desempenho com tamanhos de entrada realistas antes de assumir a complexidade extra.

Consulte também