Помогите с оптимизацией функции C # через C и / или Assembly - PullRequest
9 голосов
/ 30 мая 2010

У меня есть метод C #, который я пытаюсь оптимизировать:

// assume arrays are same dimensions
private void DoSomething(int[] bigArray1, int[] bigArray2)
{
    int data1;
    byte A1, B1, C1, D1;
    int data2;
    byte A2, B2, C2, D2;
    for (int i = 0; i < bigArray1.Length; i++)
    {
        data1 = bigArray1[i];
        data2 = bigArray2[i];

        A1 = (byte)(data1 >> 0);
        B1 = (byte)(data1 >> 8);
        C1 = (byte)(data1 >> 16);
        D1 = (byte)(data1 >> 24);

        A2 = (byte)(data2 >> 0);
        B2 = (byte)(data2 >> 8);
        C2 = (byte)(data2 >> 16);
        D2 = (byte)(data2 >> 24);

        A1 = A1 > A2 ? A1 : A2;
        B1 = B1 > B2 ? B1 : B2;
        C1 = C1 > C2 ? C1 : C2;
        D1 = D1 > D2 ? D1 : D2;

        bigArray1[i] = (A1 << 0) | (B1 << 8) | (C1 << 16) | (D1 << 24); 
    }
}

Функция в основном сравнивает два int массива. Для каждой пары совпадающих элементов метод сравнивает каждое отдельное значение байта и принимает большее из двух. Элементу в первом массиве затем присваивается новое значение int, построенное из 4 самых больших байтовых значений (независимо от источника).

Я думаю Я оптимизировал этот метод в максимально возможной степени в C # (вероятно, я не оптимизировал - предложения на этот счет также приветствуются). У меня вопрос: стоит ли мне переносить этот метод в неуправляемую DLL-библиотеку C? Будет ли результирующий метод выполняться быстрее (и насколько быстрее) с учетом издержек, связанных с маршалингом моего управляемого int массивы, чтобы их можно было передать в метод?

Если это даст мне, скажем, улучшение скорости на 10%, то это точно не будет стоить моего времени. Если бы это было в 2 или 3 раза быстрее, то, вероятно, мне пришлось бы это сделать.

Примечание: пожалуйста, никаких комментариев о "преждевременной оптимизации", заранее спасибо. Это просто «оптимизация».

Обновление: Я понял, что мой пример кода не захватил все, что я пытаюсь сделать в этой функции, поэтому вот обновленная версия:

private void DoSomethingElse(int[] dest, int[] src, double pos, 
    double srcMultiplier)
{
    int rdr;
    byte destA, destB, destC, destD;
    double rem = pos - Math.Floor(pos);
    double recipRem = 1.0 - rem;
    byte srcA1, srcA2, srcB1, srcB2, srcC1, srcC2, srcD1, srcD2;
    for (int i = 0; i < src.Length; i++)
    {
        // get destination values
        rdr = dest[(int)pos + i];
        destA = (byte)(rdr >> 0);
        destB = (byte)(rdr >> 8);
        destC = (byte)(rdr >> 16);
        destD = (byte)(rdr >> 24);
        // get bracketing source values
        rdr = src[i];
        srcA1 = (byte)(rdr >> 0);
        srcB1 = (byte)(rdr >> 8);
        srcC1 = (byte)(rdr >> 16);
        srcD1 = (byte)(rdr >> 24);
        rdr = src[i + 1];
        srcA2 = (byte)(rdr >> 0);
        srcB2 = (byte)(rdr >> 8);
        srcC2 = (byte)(rdr >> 16);
        srcD2 = (byte)(rdr >> 24);
        // interpolate (simple linear) and multiply
        srcA1 = (byte)(((double)srcA1 * recipRem) + 
            ((double)srcA2 * rem) * srcMultiplier);
        srcB1 = (byte)(((double)srcB1 * recipRem) +
            ((double)srcB2 * rem) * srcMultiplier);
        srcC1 = (byte)(((double)srcC1 * recipRem) +
            ((double)srcC2 * rem) * srcMultiplier);
        srcD1 = (byte)(((double)srcD1 * recipRem) +
            ((double)srcD2 * rem) * srcMultiplier);
        // bytewise best-of
        destA = srcA1 > destA ? srcA1 : destA;
        destB = srcB1 > destB ? srcB1 : destB;
        destC = srcC1 > destC ? srcC1 : destC;
        destD = srcD1 > destD ? srcD1 : destD;
        // convert bytes back to int
        dest[i] = (destA << 0) | (destB << 8) |
            (destC << 16) | (destD << 24);
    }
}

По сути, это делает то же самое, что и первый метод, за исключением того, что второй массив (src) всегда меньше первого (dest), а второй массив расположен дробно относительно первого ( это означает, что вместо позиции, скажем, 10 относительно dest, она может быть позиционирована в 10.682791).

Для этого мне нужно интерполировать два значения в скобках в источнике (скажем, 10 и 11 в приведенном выше примере для первого элемента), а затем сравнить интерполированные байты с байтами назначения.

Я подозреваю, что умножение, включенное в эту функцию, значительно дороже, чем сравнение байтов, так что эта часть может быть красной сельдью (извините). Кроме того, даже если сравнения все еще несколько дороги относительно умножений, у меня все еще есть проблема, что эта система может фактически быть многомерной, что означает, что вместо сравнения одномерных массивов, массивы могут быть 2-, 5- или независимо от размеров, так что в конечном итоге время, затрачиваемое на вычисление интерполированных значений, будет меньше времени, затрачиваемого на окончательное байтовое сравнение 4 байтов (я полагаю, что это так).

Насколько дорого здесь умножение по сравнению со сдвигом битов, и может ли это быть той операцией, которую можно ускорить, выгружая в DLL C (или даже DLL сборки, хотя мне пришлось бы нанять кого-нибудь создать это для меня)?

Ответы [ 6 ]

7 голосов
/ 30 мая 2010

Да, функция _mm_max_epu8 () делает то, что вы хотите. Жует по 16 байт за раз. Болевая точка - это массивы. Инструкции SSE2 требуют, чтобы их аргументы были выровнены по 16-байтовым адресам. Вы не можете вытащить это из кучи мусора, она обещает только 4-байтовое выравнивание. Даже если вы обманываете его, вычисляя смещение в массиве с выравниванием по 16 байтов, вы потеряете, когда сборщик мусора включится и переместит массив.

Вы должны будете объявить массивы в коде C / C ++, используя __declspec (align (#))). Теперь вам нужно скопировать управляемые массивы в эти неуправляемые. И результаты вернулись. То, что вы все еще впереди, зависит от деталей, которые трудно увидеть в вашем вопросе.

4 голосов
/ 30 мая 2010

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

    private static void DoOtherThing(int[] bigArray1, int[] bigArray2)
    {
        unsafe
        {
            fixed (int* p1 = bigArray1, p2=bigArray2)
            {
                byte* b1 = (byte*)p1;
                byte* b2 = (byte*)p2;
                byte* bend = (byte*)(&p1[bigArray1.Length]);
                while (b1 < bend)
                {
                    if (*b1 < *b2)
                    {
                        *b1 = *b2;
                    }
                    ++b1;
                    ++b2;
                }
            }
        }
    }

На моем компьютере, работающем под отладчиком в режиме Release с массивами в 25 миллионов дюймов, этот код примерно на 29% быстрее, чем ваш исходный. Однако при автономной работе разницы во времени выполнения практически нет. Иногда ваш оригинальный код быстрее, а иногда новый код быстрее.

Приблизительные числа:

          Debugger  Standalone
Original  1,400 ms    700 ms
My code     975 ms    700 ms

И да, я сравнил результаты, чтобы убедиться, что функции делают то же самое.

Я затрудняюсь объяснить, почему мой код не работает быстрее, поскольку он выполняет значительно меньше работы.

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

Однако следующая модификация вашего исходного кода на 10-20% быстрее.

    private static void DoSomething(int[] bigArray1, int[] bigArray2)
    {
        for (int i = 0; i < bigArray1.Length; i++)
        {
            var data1 = (uint)bigArray1[i];
            var data2 = (uint)bigArray2[i];

            var A1 = data1 & 0xff;
            var B1 = data1 & 0xff00;
            var C1 = data1 & 0xff0000;
            var D1 = data1 & 0xff000000;

            var A2 = data2 & 0xff;
            var B2 = data2 & 0xff00;
            var C2 = data2 & 0xff0000;
            var D2 = data2 & 0xff000000;

            if (A2 > A1) A1 = A2;
            if (B2 > B1) B1 = B2;
            if (C2 > C1) C1 = C2;
            if (D2 > D1) D1 = D2;

            bigArray1[i] = (int)(A1 | B1 | C1 | D1);
        }
    }
2 голосов
/ 30 мая 2010

Я не вижу способа ускорить этот код с помощью хитрых хитростей.

Если вы действительно хотите, чтобы этот код был быстрее, то единственный способ значительно (> 2х или около того) ускорить его на платформе x86, я вижу, это пойти на реализацию ассемблера / встроенных функций. SSE имеет инструкцию PCMPGTB , что

"Выполняет сравнение SIMD для большего значения упакованных байтов, слов или двойных слов в целевом операнде (первый операнд) и исходном операнде (второй операнд). Если элемент данных в операнде назначения больше, чем соответствующий элемент даты в операнде-источнике, соответствующий элемент данных в операнде-адресате установлен на все 1 с, в противном случае он установлен на все 0. "

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

С другой стороны, может быть, вы можете переформулировать свой алгоритм, чтобы вам не нужно было выбирать большие байты, а, например, взять И из операндов? Просто мысль, трудно понять, может ли она работать, не видя фактического алгоритма.

2 голосов
/ 30 мая 2010

Как насчет этого?

    private void DoSomething(int[] bigArray1, int[] bigArray2)
    {
        for (int i = 0; i < bigArray1.Length; i++)
        {
            var data1 = (uint)bigArray1[i];
            var data2 = (uint)bigArray2[i];

            bigArray1[i] = (int)(
                Math.Max(data1 & 0x000000FF, data2 & 0x000000FF) |
                Math.Max(data1 & 0x0000FF00, data2 & 0x0000FF00) |
                Math.Max(data1 & 0x00FF0000, data2 & 0x00FF0000) |
                Math.Max(data1 & 0xFF000000, data2 & 0xFF000000));
        }
    }

В нем гораздо меньше сдвига. Вы можете обнаружить, что звонки на номер Math.Max не являются встроенными, если вы их профилируете. В таком случае вы просто сделаете метод более многословным.

Я не тестировал этот код, так как у меня нет IDE со мной. Я считаю, что он делает то, что вы хотите.

Если это все еще не работает так, как вы ожидаете, вы можете попробовать использовать арифметику указателей в небезопасном блоке, но я серьезно сомневаюсь, что вы увидите выигрыш. Код, подобный этому, вряд ли будет быстрее, если вы пойдете к нему, из всего, что я прочитал. Но не верьте мне на слово. Мера, мера, мера.

Удачи.

1 голос
/ 31 мая 2010

Другой вариант для вас, если вы можете запустить Mono, это использовать пакет Mono.Simd. Это обеспечивает доступ к набору инструкций SIMD из .NET. К сожалению, вы не можете просто взять сборку и запустить ее на CLR от MS, так как обработка времени Mono выполняется особым образом во время JIT. Фактическая сборка содержит обычное IL (не SIMD) «моделирование» операций SIMD в качестве запасного варианта, если оборудование не поддерживает инструкции SIMD.

Вы также должны быть в состоянии выразить вашу проблему, используя типы, которые использует API, насколько я могу разобрать.

Вот сообщение в блоге , в котором Мигель де Иказа объявил о возможности еще в ноябре 2008 года. Довольно классная штука. Надеюсь, он будет добавлен к стандарту ECMA, и MS сможет добавить его в свой CLR.

0 голосов
/ 30 мая 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...