Algoritmo Vetorizado de Aceleração por Hardware SIMD para Inversão Horizontal de Imagens de 32 Bits (Usando YShuffleKernel de VectorTraits para Superar Limitações de Shuffle)

Em continuação ao artigo anterior sobre inversão vertical de imagens, este artigo aborda a inversão horizontal (FlipX), focando primeiramente em imagens de 32 bits para preparar o terreno para algoritmos mais complexos em imagens de 24 bits.

Serão apresentados tanto algoritmos escalares quanto vetorizados. Estes algoritmos são multiplataforma, permitindo que o mesmo código-fonte seja executado em arquiteturas como x86 (com conjuntos de instruções SSE, AVX, etc.) e ARM (com conjuntos de instruções AdvSIMD, etc.), beneficiando-se da aceleração por hardware SIMD em todas elas.

Algoritmo Escalar

Ideia do Algoritmo

A inversão horizontal, também conhecida como inversão esquerda-direita, espelha a imagem através de seu eixo vertical central. Dada uma função para acessar os pixels de origem src[x, y] e destino dst[x, y], e considerando width como a largura da imagem em pixels, a fórmula para inversão horizontal é:

dst[x, y] = src[width - 1 - x, y]

É importante notar que as coordenadas de pixel começam em 0, portanto, o pixel mais à direita tem o coordenada x de width - 1.

Essencialmente, cada pixel em uma linha é copiado na ordem inversa. Como o processamento é feito pixel a pixel, o algoritmo deve ser adaptado para diferentes tamanhos de pixel.

Detalhes do Pixel de 32 Bits

Pixels de 32 bits são convenientes para processar, pois 32 bits equivalem a 4 bytes, uma potência de dois, o que facilita a manipulação. Por isso, imagens de 32 bits são as mais frequentemente utilizadas.

Existem vários formatos de pixels de 32 bits, dependendo da ordem dos canais de cor e da presença de um canal alfa:

  • Bgr32 (também conhecido como BGRX8888, B8G8R8X8; em GDI+ como "Format32bppRgb")
  • Bgra32 (também conhecido como BGRA8888, B8G8R8A8; em GDI+ como "Format32bppArgb")
  • Pbgra32 (BGRA pré-multiplicado; em GDI+ como "Format32bppPArgb")
  • Rgb32 (também conhecido como RGBX8888, R8G8B8X8)
  • Rgba32 (também conhecido como RGBA8888, R8G8B8A8)
  • Prgba32 (RGBA pré-multiplicado)

Como o objetivo aqui é a inversão horizontal, não é necessário processar os canais de cor individualmente. O pixel inteiro pode ser tratado como uma unidade. Portanto, o algoritmo apresentado é válido para todos os formatos de pixel de 32 bits, não apenas os mencionados acima, mas também outros como CMYK32.

Implementação do Algoritmo

Com o tamanho em bytes do pixel (cbPixel) conhecido (4 bytes para 32 bits), os pixels podem ser copiados.

O código-fonte a seguir demonstra a implementação:


public static unsafe void ScalarDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
    const int cbPixel = 4; // 32 bits: Bgr32, Bgra32, Rgb32, Rgba32.
    byte* pRow = pSrc;
    byte* qRow = pDst;
    for (int i = 0; i < height; i++) {
        byte* p = pRow + (width - 1) * cbPixel; // Ponto de partida no final da linha de origem
        byte* q = qRow;                      // Ponto de partida no início da linha de destino
        for (int j = 0; j < width; j++) {
            // Copia um pixel inteiro (4 bytes)
            for (int k = 0; k < cbPixel; k++) {
                q[k] = p[k];
            }
            p -= cbPixel; // Move o ponteiro de origem para o pixel anterior
            q += cbPixel; // Move o ponteiro de destino para o próximo pixel
        }
        pRow += strideSrc; // Avança para a próxima linha de origem
        qRow += strideDst; // Avança para a próxima linha de destino
    }
}

Ao usar ponteiros, o cálculo de endereços é crucial. Para inversão horizontal, o foco está nos cálculos de endereço relacionados aos pixels dentro de uma linha (o loop interno j).

O loop interno utiliza uma estratégia de "leitura reversa, escrita sequencial":

  • A leitura começa do último pixel e, a cada iteração, o ponteiro p é deslocado para o pixel anterior. Inicialmente, p aponta para o último pixel da linha de origem.
  • A escrita começa do primeiro pixel e, a cada iteração, o ponteiro q é deslocado para o próximo pixel. Inicialmente, q aponta para o primeiro pixel da linha de destino.

Código de Teste de Referência

Utiliza-se o BenchmarkDotNet para testes de referência.


[Benchmark(Baseline = true)]
public void Scalar() {
    ScalarDo(_sourceBitmapData, _destinationBitmapData, false);
}

//[Benchmark]
public void ScalarParallel() {
    ScalarDo(_sourceBitmapData, _destinationBitmapData, true);
}

public static unsafe void ScalarDo(BitmapData src, BitmapData dst, bool useParallel = false) {
    int width = src.Width;
    int height = src.Height;
    int strideSrc = src.Stride;
    int strideDst = dst.Stride;
    byte* pSrc = (byte*)src.Scan0.ToPointer();
    byte* pDst = (byte*)dst.Scan0.ToPointer();
    // Paralelismo é habilitado se o processamento for para mais de uma linha,
    // a altura for razoável e o número de processadores for maior que 1.
    bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
    if (allowParallel) {
        // Processa cada linha em paralelo.
        Parallel.For(0, height, i => {
            int start = i;
            int len = 1;
            // Calcula o endereço de início da linha atual para origem e destino.
            byte* pSrc2 = pSrc + start * (long)strideSrc;
            byte* pDst2 = pDst + start * (long)strideDst;
            ScalarDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);
        });
    } else {
        // Processa todas as linhas sequencialmente.
        ScalarDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
    }
}

Como a inversão horizontal processa pixels dentro de uma linha, o loop externo para cada linha pode ser processado sequencialmente. O cálculo de endereço para paralelismo é relativamente simples neste caso.

Algoritmo Vetorizado

Ideia do Algoritmo

Inversão dentro de um Vetor

Enquanto o algoritmo escalar processa um byte por vez, o algoritmo vetorizado pode processar um vetor de bytes por vez, aproveitando as instruções SIMD. O principal desafio aqui é a granularidade dos vetores.

Um vetor SIMD (como Vector128) tem um tamanho mínimo de 128 bits (16 bytes). Para pixels de 32 bits (4 bytes), um vetor pode conter 4 pixels.

O primeiro obstáculo é realizar a inversão dentro de um único vetor.

Solução com .NET 7.0: Método Shuffle

Antes do .NET 7.0, não havia uma maneira direta de realizar essa operação de reordenação de elementos dentro de um vetor. A partir do .NET 7.0, os tipos de vetor como Vector128 introduziram o método Shuffle, que permite reordenar os elementos de um vetor com base em um vetor de índices.

Existem sobrecargas para diferentes tipos de elementos, como:


public static Vector128<byte> Shuffle(Vector128<byte> vector, Vector128<byte> indices);
public static Vector128<int> Shuffle(Vector128<int> vector, Vector128<int> indices);
// ...

O parâmetro indices especifica quais elementos do vetor de origem devem ser copiados para o vetor de destino. Por exemplo, vector[indices[i]].

Uma abordagem inicial seria usar a versão de Shuffle para bytes, alinhada com a lógica do algoritmo escalar. No entanto, como estamos lidando com pixels de 32 bits, é mais eficiente tratar cada pixel como uma unidade de 32 bits (um inteiro). Assim, a versão de Shuffle para int é preferível.

Para Vector128<int>, que pode conter 4 pixels de 32 bits, a inversão pode ser realizada da seguinte forma:


// Suponha que 'src' seja um Vector128<int> carregado com os pixels de origem.
Vector128<int> indices = Vector128.Create(3, 2, 1, 0); // Índices para inverter a ordem (0,1,2,3 -> 3,2,1,0)
Vector128<int> dst = Vector128.Shuffle(src, indices);

Embora funcional, essa abordagem tem uma desvantagem significativa: desempenho. A análise de descompilação revela que, até o .NET 8.0, o método Shuffle não era acelerado por hardware; em vez disso, ele usava código de fallback escalar.

Outras desvantagens do Shuffle incluem:

  • Disponível apenas para tipos de vetor de tamanho fixo (Vector128, Vector256, etc.), não para o tipo de vetor de tamanho automático (Vector).
  • Introduzido apenas no .NET 7.0, o que limita a compatibilidade com versões anteriores do .NET.
Usando VectorTraits para Superar as Limitações do Shuffle

A biblioteca VectorTraits foi desenvolvida para resolver o problema da falta de aceleração por hardware no método Shuffle. Ela utiliza instruções específicas de cada arquitetura para fornecer aceleração por hardware para operações de shuffle, incluindo:

  • x86: Utiliza instruções como _mm_shuffle_epi8.
  • Arm: Utiliza a instrução vqvtbl1q_u8.
  • Wasm: Utiliza a instrução i8x16.swizzle.

Além de suportar tipos de vetor de tamanho fixo, o VectorTraits também oferece suporte para o tipo de vetor de tamanho automático (Vector). Ele também é compatível com versões mais antigas do .NET, incluindo .NET Framework 4.5 e superior.

Para usar a aceleração por hardware com VectorTraits, basta adicionar um 's' ao final do nome do método Shuffle, transformando-o em Vector128s.Shuffle (ou usando a classe estática apropriada para o tipo de vetor). Isso indica o uso da implementação acelerada do VectorTraits.


using Zyl.VectorTraits;

// Suponha que 'src' seja um Vector128<int> carregado com os pixels de origem.
Vector128<int> indices = Vector128.Create(3, 2, 1, 0);
// Usa a versão acelerada por hardware do VectorTraits.
Vector128<int> dst = Vector128s.Shuffle(src, indices);

Usando o Tipo de Vetor de Tamanho Automático Vector<T>

Tipos de vetor de tamanho fixo como Vector128 requerem .NET Core 3.0 ou superior. Para compatibilidade com versões anteriores do .NET (a partir do .NET Framework 4.5), pode-se usar o tipo de vetor de tamanho automático Vector<T>, disponível através do pacote NuGet System.Numerics.Vectors.

O tamanho do Vector<T> é dinâmico, geralmente correspondendo ao tamanho máximo de vetor suportado pela CPU da máquina. Ele fornece a propriedade Count para indicar o número de elemnetos que ele contém.

  • Para um vetor de 128 bits, Vector<int>.Count é 4.
  • Para um vetor de 256 bits, Vector<int>.Count é 8.

A natureza dinâmica do tamanho do Vector<T> introduz um desafio para a inicialização do vetor de índices. Em vez de definir os índices explicitamente como em Vector128.Create(3, 2, 1, 0), é necessário criar um array ou Span dinamicamente e preenchê-lo com os índices corretos.

A partir do .NET Core 3.0, os construtores de Vector<T> suportam Span, permitindo o uso de alocação na pilha (stack allocation) para reduzir o overhead de memória.


// Aloca buffer na pilha para os índices.
Span<int> buf = stackalloc int[Vector<int>.Count];
// Preenche o buffer com os índices invertidos.
for (int i = 0; i < Vector<int>.Count; i++) {
    buf[i] = Vector<int>.Count - 1 - i;
}
// Cria o vetor de índices usando o buffer.
// Vectors.Create é uma utilidade do VectorTraits para compatibilidade.
Vector<int> indices = Vectors.Create(buf);

O cálculo Vector<int>.Count - 1 - i determina o índice correto para a inversão.

Como o Count de Vector<T> é fixado em tempo de execução, o cálculo do vetor de índices pode ser feito uma vez no construtor estático da classe para otimizar o desempenho, evitando recálculos repetidos.


private static readonly Vector<int> _shuffleIndices;

static ImageFlipXOn32bitBenchmark() {
    // Opção 1: Usando a sobrecarga de loop duplo do VectorTraits para inicialização.
    _shuffleIndices = Vectors.CreateByDoubleLoop<int>(Vector<int>.Count - 1, -1);
    // Opção 2: Usando Span (como mostrado anteriormente).
    /*
    Span<int> buf = stackalloc int[Vector<int>.Count];
    for (int i = 0; i < Vector<int>.Count; i++) {
        buf[i] = Vector<int>.Count - 1 - i;
    }
    _shuffleIndices = Vectors.Create(buf);
    */
}

O método Vectors.CreateByDoubleLoop do VectorTraits simplifica a inicialização de vetores com padrões sequenciais, como os índices de shuffle.

Uma vez que o vetor de índices _shuffleIndices é obtido, a operação de shuffle pode ser realizada no vetor de tamanho automático Vector<T>:


// Suponha que 'src' seja um Vector<int> carregado com os pixels de origem.
Vector<int> indices = _shuffleIndices;
Vector<int> dst = Vectors.Shuffle(src, indices);

Usando o Método YShuffleKernel para Otimização Adicional

O método Shuffle pode zerar elementos se os índices estiverem fora do intervalo. Para a inversão horizontal, todos os índices estão dentro do intervalo válido. Nesses casos, o método YShuffleKernel, também do VectorTraits, pode oferecer melhor desempenho, pois não realiza verificações de limite.


// Suponha que 'src' seja um Vector<int> carregado com os pixels de origem.
Vector<int> indices = _shuffleIndices;
// Usa YShuffleKernel para melhor desempenho em casos onde os índices são sempre válidos.
Vector<int> dst = Vectors.YShuffleKernel(src, indices);

O prefixo 'Y' nos métodos do VectorTraits indica que eles são implementações que visam otimizar o desempenho, diferenciando-os dos métodos padrão do BCL.

Invertendo uma Linha Inteira

Com a capacidade de inverter elementos dentro de um vetor, é possível inverter uma linha inteira de pixels. A estratégia de "leitura reversa, escrita sequencial" continua válida.

Se o número de bytes em uma linha for um múltiplo exato do tamanho do vetor, o processamento é simplificado. Os passos são:

  1. Inicializar o ponteiro de origem p para o final da linha de dados.
  2. Inicializar o ponteiro de destino q para o início da linha.
  3. Carregar dados da memória para um vetor usando p.
  4. Inverter o vetor usando YShuffleKernel.
  5. Escrever o vetor invertido na memória usando q.
  6. Verificar se todos os dados foram processados; se sim, terminar.
  7. Avançar os ponteiros p (para trás) e q (para frente) para o próximo vetor.
  8. Retornar ao passo 3 para continuar o loop.
  9. Concluir o processamento da linha.

Quando o número de bytes em uma linha não é um múltiplo do tamanho do vetor, o tratamento dos bytes restantes (o "pedaço final") requer lógica adicional, semelhante à abordagem do "ponteiro final" mencionada em artigos anteriores.

Implementação do Algoritmo

O código a seguir implementa o algoritmo vetorizado:


public static unsafe void UseVectorsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
    const int cbPixel = 4; // 32 bits: Bgr32, Bgra32, Rgb32, Rgba32.
    // Obtém os índices de shuffle pré-calculados.
    Vector<int> indices = _shuffleIndices;
    // Tamanho do vetor em número de elementos 'int'.
    int vectorWidthInts = Vector<int>.Count;
    // Largura da linha em bytes que é um múltiplo do tamanho do vetor.
    int widthAligned = width - (width % vectorWidthInts);
    byte* pRow = pSrc;
    byte* qRow = pDst;

    for (int i = 0; i < height; i++) {
        // Ponteiros para o início e fim da linha de origem e destino, alinhados ao tamanho do vetor.
        byte* pRowEnd = pRow + widthAligned * cbPixel;
        byte* qRowEnd = qRow + widthAligned * cbPixel;

        // Vetores para processamento em bloco.
        Vector<int>* p = (Vector<int>*)pRow;
        Vector<int>* q = (Vector<int>*)qRowEnd; // Começa do final para escrita reversa

        // Loop principal: processa blocos de vetores.
        while ((byte*)p < pRowEnd) {
            // Carrega um vetor da origem (lendo do início para o fim).
            Vector<int> data = *p;
            // Inverte o vetor usando YShuffleKernel.
            Vector<int> temp = Vectors.YShuffleKernel(data, indices);
            // Decrementa o ponteiro de destino (q aponta para o bloco a ser escrito).
            q -= 1;
            // Escreve o vetor invertido no destino (escrevendo do fim para o início).
            *q = temp;

            // Avança o ponteiro de origem para o próximo bloco.
            p++;
        }

        // Processa os bytes restantes se a largura não for múltiplo do tamanho do vetor.
        // Esta parte é simplificada aqui, mas em uma implementação completa,
        // seria necessário um loop para os bytes restantes.
        // ... tratamento para os bytes restantes ...

        pRow += strideSrc; // Avança para a próxima linha de origem
        qRow += strideDst; // Avança para a próxima linha de destino
    }
}

Nesta implementação, a estratégia de "leitura do início para o fim" e "escrita do fim para o início" é usada para alcançar a inversão horizontal com vetores. O ponteiro q é inicializado no final da linha e decrementado a cada escrita.

Código de Teste de Referência

O código de teste de referência para o algoritmo vetorizado:


[Benchmark]
public void UseVectors() {
    UseVectorsDo(_sourceBitmapData, _destinationBitmapData, false);
}

//[Benchmark]
public void UseVectorsParallel() {
    UseVectorsDo(_sourceBitmapData, _destinationBitmapData, true);
}

public static unsafe void UseVectorsDo(BitmapData src, BitmapData dst, bool useParallel = false) {
    // Tamanho do vetor em bytes.
    int vectorWidthBytes = Vector<byte>.Count;
    int width = src.Width;
    int height = src.Height;
    // Se a largura da imagem for menor ou igual ao tamanho de um vetor,
    // usa o algoritmo escalar, pois vetorizar não traria benefícios.
    if (width * sizeof(int) <= vectorWidthBytes) { // Verifica em bytes de int (32-bit pixel)
        ScalarDo(src, dst, useParallel);
        return;
    }
    int strideSrc = src.Stride;
    int strideDst = dst.Stride;
    byte* pSrc = (byte*)src.Scan0.ToPointer();
    byte* pDst = (byte*)dst.Scan0.ToPointer();
    // Lógica para paralelismo, semelhante ao ScalarDo.
    bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
    if (allowParallel) {
        Parallel.For(0, height, i => {
            int start = i;
            int len = 1;
            byte* pSrc2 = pSrc + start * (long)strideSrc;
            byte* pDst2 = pDst + start * (long)strideDst;
            UseVectorsDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);
        });
    } else {
        UseVectorsDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
    }
}

Otimização Adicional com YShuffleKernel_Args e YShuffleKernel_Core

Para otimizar ainda mais, especialmente em loops onde parte do cálculo pode ser movida para fora, o VectorTraits oferece os métodos YShuffleKernel_Args e YShuffleKernel_Core. O método _Args prepara os parâmetros, enquanto o _Core executa a operação principal, resultando em melhor desempenho quando usado em conjunto.


public static unsafe void UseVectorsArgsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
    const int cbPixel = 4; // 32 bits.
    Vector<int> indices = _shuffleIndices;
    Vector<int> args0, args1;
    // Prepara os argumentos para YShuffleKernel_Core.
    Vectors.YShuffleKernel_Args(indices, out args0, out args1);

    int vectorWidthInts = Vector<int>.Count;
    int widthAligned = width - (width % vectorWidthInts);
    byte* pRow = pSrc;
    byte* qRow = pDst;

    for (int i = 0; i < height; i++) {
        byte* pRowEnd = pRow + widthAligned * cbPixel;
        byte* qRowEnd = qRow + widthAligned * cbPixel;

        Vector<int>* p = (Vector<int>*)pRow;
        Vector<int>* q = (Vector<int>*)qRowEnd;

        while ((byte*)p < pRowEnd) {
            Vector<int> data = *p;
            // Executa a inversão usando a versão _Core otimizada.
            Vector<int> temp = Vectors.YShuffleKernel_Core(data, args0, args1);
            q -= 1;
            *q = temp;
            p++;
        }

        // ... tratamento para os bytes restantes ...

        pRow += strideSrc;
        qRow += strideDst;
    }
}

Resultados de Testes de Referência

Arquitetura x86

Os resultados dos testes de referência na arquitetura x86:

BenchmarkDotNet v0.14.0, Windows 11 ...
AMD Ryzen 7 7840H ...
.NET SDK 8.0.403
  [Host]     : .NET 8.0.10 ..., X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.10 ..., X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

| Method         | Width | Mean        | Error     | StdDev    | Ratio | RatioSD |
|--------------- |------ |------------:|----------:|----------:|------:|--------:|
| Scalar         | 1024  |    784.7 us |  14.56 us |  14.30 us |  1.00 |    0.03 |
| UseVectors     | 1024  |    106.4 us |   2.12 us |   4.96 us |  0.14 |    0.01 |
| UseVectorsArgs | 1024  |    101.4 us |   2.03 us |   3.85 us |  0.13 |    0.01 |
| ... (outros tamanhos) ...

Comparando Scalar com UseVectorsArgs para uma largura de 1024 pixels, o algoritmo vetorizado (UseVectorsArgs) é aproximadamente 7.74 vezes mais rápido que o algoritmo escalar.

Arquitetura ARM

Os mesmos testes na arquitetura ARM:

BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 ...
Apple M2 ...
.NET SDK 8.0.204
  [Host]     : .NET 8.0.4 ..., Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 8.0.4 ..., Arm64 RyuJIT AdvSIMD

| Method         | Width | Mean        | Error    | StdDev   | Ratio |
|--------------- |------ |------------:|---------:|---------:|------:|
| Scalar         | 1024  |    625.8 us |  0.81 us |  0.68 us |  1.00 |
| UseVectors     | 1024  |    151.9 us |  0.32 us |  0.27 us |  0.24 |
| UseVectorsArgs | 1024  |    151.2 us |  0.13 us |  0.12 us |  0.24 |
| ... (outros tamanhos) ...

Na arquitetura ARM, UseVectorsArgs é aproximadamente 4.14 vezes mais rápido que o algoritmo escalar.

É notável que em .NET 8.0, a diferença de performance entre UseVectors e UseVectorsArgs é mínima. Isso se deve às otimizações automáticas do compilador JIT do .NET 7.0+, que já movem parte dos cálculos para fora do loop. Em versões anteriores do .NET, a diferença seria mais pronunciada.

Resultados .NET 6.0 na Arquitetura ARM

Testes com .NET 6.0 na arquitetura ARM mostram uma diferença maior:

BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 ...
Apple M2 ...
.NET SDK 8.0.204
  [Host]     : .NET 6.0.33 ..., Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 6.0.33 ..., Arm64 RyuJIT AdvSIMD

| Method         | Width | Mean        | Error    | StdDev   | Ratio |
|--------------- |------ |------------:|---------:|---------:|------:|
| Scalar         | 1024  |  1,805.2 us |  0.72 us |  0.60 us |  1.00 |
| UseVectors     | 1024  |    454.5 us |  5.45 us |  5.10 us |  0.25 |
| UseVectorsArgs | 1024  |    158.4 us |  0.05 us |  0.04 us |  0.09 |
| ... (outros tamanhos) ...

Neste cenário, UseVectors é ~3.97x mais rápido que Scalar, e UseVectorsArgs é ~11.40x mais rápido, demonstrando o benefício da otimização _Args/_Core em versões mais antigas do .NET.

.NET Framework

Testes no .NET Framework:

BenchmarkDotNet v0.14.0, Windows 11 ...
AMD Ryzen 7 7840H ...
  [Host]     : .NET Framework 4.8.1 ..., X64 RyuJIT VectorSize=256
  DefaultJob : .NET Framework 4.8.1 ..., X64 RyuJIT VectorSize=256

| Method         | Width | Mean        | Error     | StdDev    | Ratio | RatioSD | Code Size |
|--------------- |------ |------------:|----------:|----------:|------:|--------:|----------:|
| Scalar         | 1024  |  1,315.2 us |  26.06 us |  25.59 us |  1.00 |    0.03 |   2,718 B |
| UseVectors     | 1024  |    968.2 us |  17.55 us |  16.42 us |  0.74 |    0.02 |   3,507 B |
| UseVectorsArgs | 1024  |    887.0 us |   9.91 us |   8.78 us |  0.67 |    0.01 |   3,507 B |
| ... (outros tamanhos) ...

No .NET Framework, UseVectorsArgs é aproximadamente 1.48 vezes mais rápido que o Scalar. Isso ocorre porque o .NET Framework não tem aceleração SIMD nativa para operações como Shuffle; em vez disso, ele usa código de fallback escalar. No entanto, o algoritmo escalar subjacente do VectorTraits é altamente otimizado e, ao operar em inteiros (pixels), ainda supera o algoritmo escalar baseado em bytes.

Tags: C# SIMD VectorTraits Image Processing Horizontal Flip

Publicado em 6-4 18:11 por Thomas