Оператор Собеля — внутренняя реализация SIMD x86

Я изучаю C # .NET 5 Intrinsics и интересуюсь передовыми практиками. Сейчас действительно сложно найти достаточно информации о том, как инструкции SIMD (логически / внутренне) работают в .NET. Кроме того, я не знаком с C ++ и языками ассемблера.

Цель решения — подать заявку Оператор Собеля фильтр к загруженному изображению. Для операций с изображениями я использовал System.Drawing.Common Пакет NuGet. Таким образом, решение предназначено только для Windows.

В SobelOperator Класс содержит две реализации оператора Собеля:

  1. SobelOperatorScalar — Скалярное решение, которое можно использовать в качестве запасного варианта, если текущий ЦП несовместим с AVX2.
  2. SobelOperatorSimd — Решение SIMD x86 для аппаратного ускорения. — цель обзора

Код для обзора

public interface ISobelOperator
{
    Bitmap Apply(Bitmap bmp);
}

public class SobelOperator : ISobelOperator
{
    private static Color[] _grayPallette;
    private readonly ISobelOperator _operator;

    public bool IsHardwareAccelerated { get; } 
            
    public SobelOperator(bool hardwareAccelerated = true)
    {
        if (_grayPallette == null)
            _grayPallette = Enumerable.Range(0, 256).Select(i => Color.FromArgb(i, i, i)).ToArray();

        IsHardwareAccelerated = hardwareAccelerated && Avx2.IsSupported;

        _operator = IsHardwareAccelerated ? new SobelOperatorSimd() : new SobelOperatorScalar();
    }

    public Bitmap Apply(Bitmap bmp) 
        => _operator.Apply(bmp);

    private class SobelOperatorSimd : ISobelOperator
    {
        private const byte m0 = 0b01001001;
        private const byte m1 = 0b10010010;
        private const byte m2 = 0b00100100;

        //0.299R + 0.587G + 0.114B
        private readonly Vector256<float> bWeight = Vector256.Create(0.114f);
        private readonly Vector256<float> gWeight = Vector256.Create(0.587f);
        private readonly Vector256<float> rWeight = Vector256.Create(0.299f);
        private readonly Vector256<int> bMut = Vector256.Create(0, 3, 6, 1, 4, 7, 2, 5);
        private readonly Vector256<int> gMut = Vector256.Create(1, 4, 7, 2, 5, 0, 3, 6);
        private readonly Vector256<int> rMut = Vector256.Create(2, 5, 0, 3, 6, 1, 4, 7);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private unsafe Vector256<int> GetBrightness(byte* ptr)
        {
            Vector256<int> v0 = Avx2.ConvertToVector256Int32(ptr);
            Vector256<int> v1 = Avx2.ConvertToVector256Int32(ptr + 8);
            Vector256<int> v2 = Avx2.ConvertToVector256Int32(ptr + 16);

            Vector256<int> vb = Avx2.Blend(Avx2.Blend(v0, v1, m1), v2, m2);
            vb = Avx2.PermuteVar8x32(vb, bMut);
            Vector256<int> vg = Avx2.Blend(Avx2.Blend(v0, v1, m2), v2, m0);
            vg = Avx2.PermuteVar8x32(vg, gMut);
            Vector256<int> vr = Avx2.Blend(Avx2.Blend(v0, v1, m0), v2, m1);
            vr = Avx2.PermuteVar8x32(vr, rMut);
            Vector256<float> vfb = Avx.Multiply(Avx.ConvertToVector256Single(vb), bWeight);
            Vector256<float> vfg = Avx.Multiply(Avx.ConvertToVector256Single(vg), gWeight);
            Vector256<float> vfr = Avx.Multiply(Avx.ConvertToVector256Single(vr), rWeight);
            return Avx.ConvertToVector256Int32WithTruncation(Avx.Add(Avx.Add(vfb, vfg), vfr));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private unsafe void ToGrayscale(byte* srcPtr, byte* dstPtr, int pixelsCount)
        {
            byte* tail = srcPtr + (pixelsCount & -16) * 3;
            byte* srcEnd = srcPtr + pixelsCount * 3;
            byte* dstEnd = dstPtr + pixelsCount;

            while (true)
            {
                while (srcPtr < tail)
                {
                    Vector256<int> vi0 = GetBrightness(srcPtr);
                    Vector256<int> vi1 = GetBrightness(srcPtr + 24);
                    Vector128<short> v0 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(vi0, 0), Avx2.ExtractVector128(vi0, 1));
                    Vector128<short> v1 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(vi1, 0), Avx2.ExtractVector128(vi1, 1));
                    Sse2.Store(dstPtr, Sse2.PackUnsignedSaturate(v0, v1));
                    srcPtr += 48;
                    dstPtr += 16;
                }
                if (srcPtr == srcEnd)
                    break;
                tail = srcEnd;
                srcPtr = srcEnd - 48;
                dstPtr = dstEnd - 16;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe Vector128<byte> ApplySobelKernel(byte* srcPtr, int width)
        {
            Vector256<short> v00 = Avx2.ConvertToVector256Int16(srcPtr);
            Vector256<short> v01 = Avx2.ConvertToVector256Int16(srcPtr + 1);
            Vector256<short> v02 = Avx2.ConvertToVector256Int16(srcPtr + 2);
            Vector256<short> v10 = Avx2.ConvertToVector256Int16(srcPtr + width);
            Vector256<short> v12 = Avx2.ConvertToVector256Int16(srcPtr + width + 2);
            Vector256<short> v20 = Avx2.ConvertToVector256Int16(srcPtr + width * 2);
            Vector256<short> v21 = Avx2.ConvertToVector256Int16(srcPtr + width * 2 + 1);
            Vector256<short> v22 = Avx2.ConvertToVector256Int16(srcPtr + width * 2 + 2);

            Vector256<short> vgx = Avx2.Subtract(v02, v00);
            vgx = Avx2.Subtract(vgx, Avx2.ShiftLeftLogical(v10, 1));
            vgx = Avx2.Add(vgx, Avx2.ShiftLeftLogical(v12, 1));
            vgx = Avx2.Subtract(vgx, v20);
            vgx = Avx2.Add(vgx, v22);

            Vector256<short> vgy = Avx2.Add(v00, Avx2.ShiftLeftLogical(v01, 1));
            vgy = Avx2.Add(vgy, v02);
            vgy = Avx2.Subtract(vgy, v20);
            vgy = Avx2.Subtract(vgy, Avx2.ShiftLeftLogical(v21, 1));
            vgy = Avx2.Subtract(vgy, v22);

            // sqrt(vgx * vgx + vgy * vgy)
            Vector256<short> vgp0 = Avx2.UnpackLow(vgx, vgy);
            Vector256<short> vgp1 = Avx2.UnpackHigh(vgx, vgy);
            Vector256<int> v0 = Avx2.MultiplyAddAdjacent(vgp0, vgp0);
            Vector256<int> v1 = Avx2.MultiplyAddAdjacent(vgp1, vgp1);
            Vector256<int> gt0 = Avx.ConvertToVector256Int32WithTruncation(Avx.Sqrt(Avx.ConvertToVector256Single(v0)));
            Vector256<int> gt1 = Avx.ConvertToVector256Int32WithTruncation(Avx.Sqrt(Avx.ConvertToVector256Single(v1)));

            Vector128<short> gts0 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(gt0, 0), Avx2.ExtractVector128(gt1, 0));
            Vector128<short> gts1 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(gt0, 1), Avx2.ExtractVector128(gt1, 1));
            return Sse2.PackUnsignedSaturate(gts0, gts1);
        }


        public Bitmap Apply(Bitmap bmp)
        {
            int width = bmp.Width;
            int height = bmp.Height;
            int pixelsCount = width * height;
            byte[] buffer = new byte[pixelsCount];

            Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
            Bitmap outBmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
            ColorPalette pal = outBmp.Palette;
            for (int i = 0; i < 256; i++)
                pal.Entries[i] = _grayPallette[i];
            outBmp.Palette = pal;

            unsafe
            {
                fixed (byte* bufPtr = buffer)
                {
                    BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
                    ToGrayscale((byte*)bmpData.Scan0.ToPointer(), bufPtr, pixelsCount);
                    bmp.UnlockBits(bmpData);

                    BitmapData outBmpData = outBmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
                    byte* dstPtr = (byte*)outBmpData.Scan0.ToPointer();

                    int length = pixelsCount - width * 2 - 1;
                    byte* tail = bufPtr + (length & -16);
                    byte* srcPos = bufPtr;
                    byte* srcEnd = bufPtr + length;
                    byte* dstPos = dstPtr + width + 1;
                    byte* dstEnd = dstPos + length;

                    while (true)
                    {
                        while (srcPos < tail)
                        {
                            Sse2.Store(dstPos, ApplySobelKernel(srcPos, width));
                            srcPos += 16;
                            dstPos += 16;
                        }

                        if (srcPos == srcEnd)
                            break;
                        tail = srcEnd;
                        srcPos = srcEnd - 16;
                        dstPos = dstEnd - 16;
                    }

                    for (dstPos = dstPtr + width; dstPos <= dstPtr + pixelsCount - width; dstPos += width)
                    {
                        *dstPos-- = 0;
                        *dstPos++ = 0;
                    }

                    outBmp.UnlockBits(outBmpData);
                }
            }

            return outBmp;
        }
    }

    private class SobelOperatorScalar : ISobelOperator
    {
        public Bitmap Apply(Bitmap bmp)
        {
            BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
            int strideLength = bmpData.Stride * bmpData.Height;
            byte[] buffer = new byte[Math.Abs(strideLength)];
            Marshal.Copy(bmpData.Scan0, buffer, 0, strideLength);
            bmp.UnlockBits(bmpData);

            int width = bmp.Width;
            int height = bmp.Height;
            int pixelsCount = width * height;
            byte[] pixelBuffer = new byte[pixelsCount];
            byte[] resultBuffer = new byte[pixelsCount];

            //0.299R + 0.587G + 0.114B
            for (int i = 0; i < pixelsCount; i++)
            {
                int offset = i * 3;
                byte brightness = (byte)(buffer[offset] * 0.114f + buffer[offset + 1] * 0.587f + buffer[offset + 2] * 0.299f);
                pixelBuffer[i] = brightness;
            }

            for (int i = width + 1; i < pixelsCount - width - 1; i++)
            {
                if (i % width == width - 1)
                    i += 2;

                int gx = -pixelBuffer[i - 1 - width] + pixelBuffer[i + 1 - width] - 2 * pixelBuffer[i - 1] +
                    2 * pixelBuffer[i + 1] - pixelBuffer[i - 1 + width] + pixelBuffer[i + 1 + width];

                int gy = pixelBuffer[i - 1 - width] + 2 * pixelBuffer[i - width] + pixelBuffer[i + 1 - width] -
                    pixelBuffer[i - 1 + width] - 2 * pixelBuffer[i + width] - pixelBuffer[i + 1 + width];

                int gt = (int)MathF.Sqrt(gx * gx + gy * gy);
                if (gt > byte.MaxValue) gt = byte.MaxValue;

                resultBuffer[i] = (byte)gt;
            }

            Bitmap outBmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
            BitmapData outBmpData = outBmp.LockBits(new Rectangle(Point.Empty, outBmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
            Marshal.Copy(resultBuffer, 0, outBmpData.Scan0, outBmpData.Stride * outBmpData.Height);
            outBmp.UnlockBits(outBmpData);
            ColorPalette pal = outBmp.Palette;
            for (int i = 0; i < 256; i++)
                pal.Entries[i] = _grayPallette[i];
            outBmp.Palette = pal;
            return outBmp;
        }
    }
}

Выходной тест

Тестовое изображение
исходное изображение

Program.cs

static void Main(string[] args)
{
    const string fileName = "image.jpg";
    Bitmap bmp = new Bitmap(fileName);

    SobelOperator sobelOperator = new SobelOperator();
    Console.WriteLine($"SIMD accelerated: {(sobelOperator.IsHardwareAccelerated ? "Yes" : "No")}");
    Bitmap result = sobelOperator.Apply(bmp);
    result.Save("out.jpg", ImageFormat.Jpeg);

    Console.WriteLine("Done.");
    Console.ReadKey();
}

Консольный вывод

SIMD accelerated: Yes
Done.

Выходное изображение
введите описание изображения здесь

Выходные изображения реализаций Scalar и SIMD бинарно идентичны.

Benchmark.NET

[MemoryDiagnoser]
public class MyBenchmark
{
    private readonly ISobelOperator _sobelOperator = new SobelOperator();
    private readonly ISobelOperator _sobelOperatorSw = new SobelOperator(false);
    private readonly Bitmap bmp = new Bitmap(@"C:Sourceimage.jpg");

    [Benchmark(Description = "SIMD Enabled")]
    public Bitmap TestSimd()
    {
        return _sobelOperator.Apply(bmp);
    }

    [Benchmark(Description = "SIMD Disabled")]
    public Bitmap TestScalar()
    {
        return _sobelOperatorSw.Apply(bmp);
    }
}

static void Main(string[] args)
{
    var summary = BenchmarkRunner.Run<MyBenchmark>();
    Console.ReadKey();
}
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.101
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT

|          Method |      Mean |     Error |    StdDev |    Gen 0 |    Gen 1 |    Gen 2 | Allocated |
|---------------- |----------:|----------:|----------:|---------:|---------:|---------:|----------:|
|  'SIMD Enabled' |  7.285 ms | 0.1165 ms | 0.1089 ms | 992.1875 | 992.1875 | 992.1875 |   3.35 MB |
| 'SIMD Disabled' | 48.412 ms | 0.2312 ms | 0.2162 ms | 454.5455 | 454.5455 | 454.5455 |  16.61 MB |

Решение Intrinsics в ~ 6,6 раза быстрее. И в целом ест меньше памяти, потому что unsafe и не использует Marshal.Copy загрузить / сохранить byte[] буферы.

1 ответ
1

Баги с изображениями неудобной ширины

Этот код:

Bitmap outBmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
BitmapData outBmpData = outBmp.LockBits(new Rectangle(Point.Empty, outBmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
Marshal.Copy(resultBuffer, 0, outBmpData.Scan0, outBmpData.Stride * outBmpData.Height);

Отлично работает, если ширина изображения кратна 4. Но в других случаях это не работает. Проблема в том, что растровые изображения имеют шаг, кратный 4, если их «естественная ширина в байтах» не кратна 4, то в конце каждой строки будет заполнение. Этот код не учитывает это, поэтому для изображений с заполнением он пытается скопировать больше данных из буфера результатов, чем на самом деле в нем, что вызывает исключение. Даже если бы это сработало, это все равно было бы неправильным, в результате каждая строка пикселей «теряла» некоторые пиксели из-за заполнения, срезая изображение.

Эта проблема также влияет на другие места, где растровое изображение заблокировано с размером пикселя, отличным от 4 байтов, но влияет на них по-разному.

Обычно это означает, что нам нужно работать с RGB и 8-битными данными изображения строка за строкой, как бы это ни раздражало. ARGB лучше в этом отношении, но блокировка изображения RGB в формате ARGB имеет большие затраты — с точки зрения производительности это намного хуже, чем принуждение к работе с данными RGB (но код, который мы должны написать для работы с RGB, действительно неприятен ..) В вашем коде есть несколько действительно хороших циклов, жаль, что их приходится заменять на более уродливые, по крайней мере, если вы хотите, чтобы работали изображения разной ширины.

Еще одна маленькая деталь: очень маленькая ширина не работайте с трюком «отступить от конца», чтобы выполнить последнюю итерацию. В этих случаях вы также можете вернуться к скалярной реализации — на SIMD нет надежды, и это все равно маленькие изображения.

Улучшения производительности, которые могут изменить результат

Что касается тестов, я отмечу, что вы используете Intel Haswell, поэтому я также тестировал Intel Haswell. Это может иметь значение: если код A лучше, чем код B на Haswell, ситуация может быть обратной на Skylake или AMD Ryzen.

Квадратный корень в ядре Собеля можно заменить на Avx.Multiply(Avx.ReciprocalSqrt(v0f), v0f) где v0f является v0 преобразованы в плавающие. Обратный квадратный корень имеет точность от 11 до 12 бит, и результат в любом случае преобразуется в 8 бит на пиксель. На тестовом изображении это не повлияло на результаты, но немного ускорило ядро. В общем, я не могу гарантировать, что есть никогда разница в результате, особенно при извлечении квадратного корня из идеального квадрата (который находится прямо на острие неправильного усечения, если обратный квадратный корень равен только просто слишком низко).

Преобразование в оттенки серого также можно выполнить немного быстрее. Основная идея там, в некоторой степени основанная на этот код, заключается в использовании Avx2.MultiplyHigh на векторе ushort вместо умножения с плавающей запятой. Все остальное происходит в поддержку этого. Однако, как это работает, векторы заканчиваются «потраченными впустую битами», когда результат вычислений отбрасывается. Из-за этого он по-прежнему обрабатывает до 6 инструкций умножения на 16 байтов вывода, хотя на инструкцию выполняется в два раза больше умножений. Таким образом, с точки зрения количества инструкций умножения оно не лучше, чем было раньше, и вдобавок к этому целочисленное умножение Haswell имеет половину пропускной способности по сравнению с умножением с плавающей запятой.

Остальная часть функции также не кажется особенно хорошей, поскольку она чрезвычайно перегружена перемешиванием (Haswell может выполнять только одно перемешивание за цикл, поэтому количество перемешиваний важно). На самом деле я не знаю Почему это быстрее, я могу только сказать вам, что так говорилось в тестах и ​​что разница была значительной, несмотря на относительно высокую временную дисперсию, которую я продолжал наблюдать. Надеюсь, что возможен лучший подход, но я не знаю, что это такое.

const ushort rw = 19595;
const ushort gw = 38470;
const ushort bw = 7471;
Vector256<ushort> rW = Vector256.Create(rw);
Vector256<ushort> gW = Vector256.Create(gw);
Vector256<ushort> bW = Vector256.Create(bw);
const byte _ = 0x80;
Vector256<byte> shuf0 = Vector256.Create(
    _, 0, _, 3, _, 6, _, 9, _, 12, _, _, _, _, _, _,
    _, 0, _, 3, _, 6, _, 9, _, 12, _, _, _, _, _, _);
Vector256<byte> shuf1 = Vector256.Create(
    _, 1, _, 4, _, 7, _, 10, _, 13, _, _, _, _, _, _,
    _, 1, _, 4, _, 7, _, 10, _, 13, _, _, _, _, _, _);
Vector256<byte> shuf2 = Vector256.Create(
    _, 2, _, 5, _, 8, _, 11, _, 14, _, _, _, _, _, _,
    _, 2, _, 5, _, 8, _, 11, _, 14, _, _, _, _, _, _);
Vector256<byte> shuf3 = Vector256.Create(
    1, 3, 5, 7, 9, _, _, _, _, _, _, _, _, _, _, _,
    _, _, _, _, _, 1, 3, 5, 7, 9, _, _, _, _, _, _);
Vector256<byte> shuf4 = Vector256.Create(
    _, _, _, _, _, _, _, _, _, _, 9, _, _, _, _, _,
    _, _, _, _, _, _, _, _, _, _, _, 1, 3, 5, 7, 9);
Vector256<ushort> bias = Vector256.Create((ushort)0x02);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe Vector128<byte> GetBrightness16(byte* ptr)
{
    var raw0 = Avx2.LoadVector128(ptr);
    var raw1 = Avx2.LoadVector128(ptr + 16);
    var raw2 = Avx2.LoadVector128(ptr + 32);
    var partA = Avx2.InsertVector128(raw0.ToVector256(), Avx2.AlignRight(raw1, raw0, 15), 1);
    var partB = Avx2.InsertVector128(Avx2.AlignRight(raw2, raw1, 1).ToVector256(), raw2, 1);
    partB = Avx2.AlignRight(partB, partB, 1);

    var b0 = Avx2.Shuffle(partA, shuf0).AsUInt16();
    var g0 = Avx2.Shuffle(partA, shuf1).AsUInt16();
    var r0 = Avx2.Shuffle(partA, shuf2).AsUInt16();
    var b1 = Avx2.Shuffle(partB, shuf0).AsUInt16();
    var g1 = Avx2.Shuffle(partB, shuf1).AsUInt16();
    var r1 = Avx2.Shuffle(partB, shuf2).AsUInt16();

    b0 = Avx2.MultiplyHigh(b0, bW);
    g0 = Avx2.MultiplyHigh(g0, gW);
    r0 = Avx2.MultiplyHigh(r0, rW);
    b1 = Avx2.MultiplyHigh(b1, bW);
    g1 = Avx2.MultiplyHigh(g1, gW);
    r1 = Avx2.MultiplyHigh(r1, rW);
    var sum0 = Avx2.AddSaturate(Avx2.AddSaturate(Avx2.Add(b0, g0), r0), bias);
    var shufsum0 = Avx2.Shuffle(sum0.AsByte(), shuf3);
    var sum1 = Avx2.AddSaturate(Avx2.AddSaturate(Avx2.Add(b1, g1), r1), bias);
    var shufsum1 = Avx2.Shuffle(sum1.AsByte(), shuf4);
    var shufsum = Avx2.Or(shufsum0, shufsum1);
    return Avx2.Or(Vector256.GetLower(shufsum), Vector256.GetUpper(shufsum0));
}

Кстати, я выбрал смещение, чтобы минимизировать разницу с исходным преобразованием оттенков серого. Обычно я бы использовал смещение 0x80 для равномерного округления. Веса цветовых каналов являются приблизительными, в 65536 раз превышающими их вес с плавающей запятой, выбранный таким образом, чтобы в сумме они составляли 65536. MultiplyHigh неявно делит на 65536, поэтому эти шкалы эффективно работают как их аналоги с плавающей запятой. Они не иметь кстати, чтобы добавить ровно 65536: благодаря насыщающим добавкам (которые не требуют дополнительных затрат по сравнению с обычными добавками) ничего плохого не случится, если они добавят немного больше. Вы можете использовать это свойство, чтобы настроить их так, чтобы они более точно соответствовали исходному преобразованию.

В качестве альтернативы можно было бы легко адаптировать скалярную функцию к арифметике с фиксированной точкой, чтобы она давала те же результаты, что и эта версия преобразователя оттенков серого SIMD.

Я попытался использовать невыровненные нагрузки для замены Avx2.AlignRight (он же vpalignr), но тест показал, что это было немного медленнее.

Преобразование оттенков серого можно основать на vpmaddubsw (Avx2.MultiplyAddAdjacent с векторами байта и sbyte). Для данных ARGB это очень быстро, хотя и заметно менее точно. Я не работал с этим для данных RGB.

Маленькие вещи

ExtractVector128-снижающая половина вектора

Если вы напишете Avx2.ExtractVector128(gt0, 0), вы получаете то, о чем просили, VEXTRACTI128 с нулевым индексом. Однако это не лучшая инструкция по применению, лучшая инструкция по применению — ничего. Никаких инструкций не требуется. Что должно произойти, так это то, что следующая операция будет использовать только 128-битную версию того же векторного регистра. Чтобы указать C # это сделать, Vector256.GetLower.

Это контрастирует, например, с C ++, где компиляторы обычно делают эту замену самостоятельно.

Разница в производительности от этого, если таковая была, была незначительной. Однако сохранение явной инструкции извлечения может только помочь.

Ненужный intptr.ToPointer()

В разное время код содержит (byte*)intptr.ToPointer() или какой-то вариант. Достаточно привести к указателю, вызвав ToPointer() ничего не добавляет к этому коду.

  • $ begingroup $
    Even if it had worked it still would have been wrong не могу согласиться, потому что PixelFormat устанавливается явно Format8bppIndexed — это означает 1 байт на пиксель. По логике, здесь нет права на ошибку или исключение. Соблюдение выравнивания данных строки пикселей имеет смысл только в том случае, если вы записываете файл формата BMP непосредственно в файл. Это не влияет BmpData.Scan0
    $ endgroup $
    — эспот
    1 час назад

  • $ begingroup $
    Это не влияет на Scan0, но влияет на конец каждой строки, потому что в этом случае Stride! = Width. Например, изображение шириной 301 будет иметь шаг 304, если оно заблокировано с форматом 8 бит / пиксель.
    $ endgroup $
    — Гарольд
    1 час назад

  • $ begingroup $
    2) Generally it means that we need to work with RGB — Я именно так и делаю, действительно работает быстрее, чем ARGB. Моя первая версия ToGrayscale метод работал с целочисленным умножением и форматом ARGB 32bpp. Тело этого метода действительно работало быстрее, общая производительность байт была хуже примерно на 2 мс для изображения, показанного в сообщении. Я выбрал подход RGB с Blend тогда. 3) small widths don't work with the "step back from the end"-trick хорошее наблюдение. Но я не думаю, что изображение размером менее 16 пикселей применимо для фильтрации с помощью оператора Собеля, потому что его ядру для работы требуется как минимум 3 строки.
    $ endgroup $
    — эспот
    1 час назад

  • $ begingroup $
    image with width 301 would have a stride of 304 — Понял, спасибо! Проверим.
    $ endgroup $
    — эспот
    1 час назад

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *