Самая быстрая операция обратного чередования в C?

У меня есть указатель на массив байтов mixed, который содержит чередующиеся байты двух разных массивов array1 и array2. Скажем, mixed выглядит примерно так:

a1b2c3d4...

Что мне нужно сделать, так это отменить чередование байтов, чтобы я получил array1 = abcd... и array2 = 1234.... Я знаю длину mixed заранее, а длины array1 и array2 эквивалентны, обе равны mixed / 2.

Вот моя текущая реализация (array1 и array2 уже выделены):

int i, j;
int mixedLength_2 = mixedLength / 2;
for (i = 0, j = 0; i < mixedLength_2; i++, j += 2)
{
    array1[i] = mixed[j];
    array2[i] = mixed[j+1];
}

Это позволяет избежать дорогостоящих операций умножения или деления, но все равно работает недостаточно быстро. Я надеюсь, что есть что-то вроде memcpy, которое использует индексатор, который может использовать низкоуровневые операции блочного копирования для ускорения процесса. Есть ли более быстрая реализация, чем у меня сейчас?

Изменить

Целевая платформа — Objective-C для iOS и Mac. Быстрая работа важнее для iOS-устройств, поэтому решение, ориентированное конкретно на iOS, будет лучше, чем ничего.

Обновить

Спасибо всем за ответы, особенно Стивену Кэнону, Грэму Ли и Меки. Вот моя «главная» функция, которая использует встроенные функции Стивена NEON, если они доступны, и в противном случае курсоры объединения Грэма с уменьшенным количеством итераций, как это было предложено Меки.

void interleave(const uint8_t *srcA, const uint8_t *srcB, uint8_t *dstAB, size_t dstABLength)
{
#if defined __ARM_NEON__
    // attempt to use NEON intrinsics

    // iterate 32-bytes at a time
    div_t dstABLength_32 = div(dstABLength, 32);
    if (dstABLength_32.rem == 0)
    {
        while (dstABLength_32.quot --> 0)
        {
            const uint8x16_t a = vld1q_u8(srcA);
            const uint8x16_t b = vld1q_u8(srcB);
            const uint8x16x2_t ab = { a, b };
            vst2q_u8(dstAB, ab);
            srcA += 16;
            srcB += 16;
            dstAB += 32;
        }
        return;
    }

    // iterate 16-bytes at a time
    div_t dstABLength_16 = div(dstABLength, 16);
    if (dstABLength_16.rem == 0)
    {
        while (dstABLength_16.quot --> 0)
        {
            const uint8x8_t a = vld1_u8(srcA);
            const uint8x8_t b = vld1_u8(srcB);
            const uint8x8x2_t ab = { a, b };
            vst2_u8(dstAB, ab);
            srcA += 8;
            srcB += 8;
            dstAB += 16;
        }
        return;
    }
#endif

    // if the bytes were not aligned properly
    // or NEON is unavailable, fall back to
    // an optimized iteration

    // iterate 8-bytes at a time
    div_t dstABLength_8 = div(dstABLength, 8);
    if (dstABLength_8.rem == 0)
    {
        typedef union
        {
            uint64_t wide;
            struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; uint8_t a3; uint8_t b3; uint8_t a4; uint8_t b4; } narrow;
        } ab8x8_t;

        uint64_t *dstAB64 = (uint64_t *)dstAB;
        int j = 0;
        for (int i = 0; i < dstABLength_8.quot; i++)
        {
            ab8x8_t cursor;
            cursor.narrow.a1 = srcA[j  ];
            cursor.narrow.b1 = srcB[j++];
            cursor.narrow.a2 = srcA[j  ];
            cursor.narrow.b2 = srcB[j++];
            cursor.narrow.a3 = srcA[j  ];
            cursor.narrow.b3 = srcB[j++];
            cursor.narrow.a4 = srcA[j  ];
            cursor.narrow.b4 = srcB[j++];
            dstAB64[i] = cursor.wide;
        }
        return;
    }

    // iterate 4-bytes at a time
    div_t dstABLength_4 = div(dstABLength, 4);
    if (dstABLength_4.rem == 0)
    {
        typedef union
        {
            uint32_t wide;
            struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; } narrow;
        } ab8x4_t;

        uint32_t *dstAB32 = (uint32_t *)dstAB;
        int j = 0;
        for (int i = 0; i < dstABLength_4.quot; i++)
        {
            ab8x4_t cursor;
            cursor.narrow.a1 = srcA[j  ];
            cursor.narrow.b1 = srcB[j++];
            cursor.narrow.a2 = srcA[j  ];
            cursor.narrow.b2 = srcB[j++];
            dstAB32[i] = cursor.wide;
        }
        return;
    }

    // iterate 2-bytes at a time
    div_t dstABLength_2 = div(dstABLength, 2);
    typedef union
    {
        uint16_t wide;
        struct { uint8_t a; uint8_t b; } narrow;
    } ab8x2_t;

    uint16_t *dstAB16 = (uint16_t *)dstAB;
    for (int i = 0; i < dstABLength_2.quot; i++)
    {
        ab8x2_t cursor;
        cursor.narrow.a = srcA[i];
        cursor.narrow.b = srcB[i];
        dstAB16[i] = cursor.wide;
    }
}

void deinterleave(const uint8_t *srcAB, uint8_t *dstA, uint8_t *dstB, size_t srcABLength)
{
#if defined __ARM_NEON__
    // attempt to use NEON intrinsics

    // iterate 32-bytes at a time
    div_t srcABLength_32 = div(srcABLength, 32);
    if (srcABLength_32.rem == 0)
    {
        while (srcABLength_32.quot --> 0)
        {
            const uint8x16x2_t ab = vld2q_u8(srcAB);
            vst1q_u8(dstA, ab.val[0]);
            vst1q_u8(dstB, ab.val[1]);
            srcAB += 32;
            dstA += 16;
            dstB += 16;
        }
        return;
    }

    // iterate 16-bytes at a time
    div_t srcABLength_16 = div(srcABLength, 16);
    if (srcABLength_16.rem == 0)
    {
        while (srcABLength_16.quot --> 0)
        {
            const uint8x8x2_t ab = vld2_u8(srcAB);
            vst1_u8(dstA, ab.val[0]);
            vst1_u8(dstB, ab.val[1]);
            srcAB += 16;
            dstA += 8;
            dstB += 8;
        }
        return;
    }
#endif

    // if the bytes were not aligned properly
    // or NEON is unavailable, fall back to
    // an optimized iteration

    // iterate 8-bytes at a time
    div_t srcABLength_8 = div(srcABLength, 8);
    if (srcABLength_8.rem == 0)
    {
        typedef union
        {
            uint64_t wide;
            struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; uint8_t a3; uint8_t b3; uint8_t a4; uint8_t b4; } narrow;
        } ab8x8_t;

        uint64_t *srcAB64 = (uint64_t *)srcAB;
        int j = 0;
        for (int i = 0; i < srcABLength_8.quot; i++)
        {
            ab8x8_t cursor;
            cursor.wide = srcAB64[i];
            dstA[j  ] = cursor.narrow.a1;
            dstB[j++] = cursor.narrow.b1;
            dstA[j  ] = cursor.narrow.a2;
            dstB[j++] = cursor.narrow.b2;
            dstA[j  ] = cursor.narrow.a3;
            dstB[j++] = cursor.narrow.b3;
            dstA[j  ] = cursor.narrow.a4;
            dstB[j++] = cursor.narrow.b4;
        }
        return;
    }

    // iterate 4-bytes at a time
    div_t srcABLength_4 = div(srcABLength, 4);
    if (srcABLength_4.rem == 0)
    {
        typedef union
        {
            uint32_t wide;
            struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; } narrow;
        } ab8x4_t;

        uint32_t *srcAB32 = (uint32_t *)srcAB;
        int j = 0;
        for (int i = 0; i < srcABLength_4.quot; i++)
        {
            ab8x4_t cursor;
            cursor.wide = srcAB32[i];
            dstA[j  ] = cursor.narrow.a1;
            dstB[j++] = cursor.narrow.b1;
            dstA[j  ] = cursor.narrow.a2;
            dstB[j++] = cursor.narrow.b2;
        }
        return;
    }

    // iterate 2-bytes at a time
    div_t srcABLength_2 = div(srcABLength, 2);
    typedef union
    {
        uint16_t wide;
        struct { uint8_t a; uint8_t b; } narrow;
    } ab8x2_t;

    uint16_t *srcAB16 = (uint16_t *)srcAB;
    for (int i = 0; i < srcABLength_2.quot; i++)
    {
        ab8x2_t cursor;
        cursor.wide = srcAB16[i];
        dstA[i] = cursor.narrow.a;
        dstB[i] = cursor.narrow.b;
    }
}

person Anton    schedule 28.01.2013    source источник
comment
Ну, если ввод действительно чередуется, то вы не можете копировать блок...   -  person    schedule 28.01.2013
comment
На какие платформы вы ориентируетесь? Многие имеют хорошо оптимизированные библиотечные функции для выполнения этих операций. Однако в стандартной библиотеке C ничего нет.   -  person Stephen Canon    schedule 28.01.2013
comment
@StephenCanon: Objective-C для iOS/Mac. Эта оптимизация особенно важна для iOS.   -  person Anton    schedule 28.01.2013
comment
@Anton: имеется в виду iOS и OS X, или вас интересуют и другие платформы?   -  person Stephen Canon    schedule 28.01.2013
comment
@StephenCanon: отредактировал мой комментарий, чтобы уточнить - iOS и OS X.   -  person Anton    schedule 28.01.2013
comment
@ H2CO3: memcpy не будет работать, но я надеюсь на что-то столь же быстрое.   -  person Anton    schedule 28.01.2013
comment
Не должно быть такого большого улучшения, но вместо i < mixedLength / 2 вы можете написать j < mixedLength и сохранить деление на итерацию без использования временной переменной.   -  person Idan Arye    schedule 28.01.2013
comment
@IdanArye: Спасибо, я соответственно обновил код. Вы правы - это недостаточное улучшение.   -  person Anton    schedule 28.01.2013
comment
Вы можете попробовать прочитать исходный массив как массив коротких (2-байтовых величин) или, возможно, даже 4- или 8-байтовых целых чисел. Храните, извлекая четные и нечетные половины со сдвигами и масками. Не очень портативный, но должен обеспечить некоторую скорость.   -  person n. 1.8e9-where's-my-share m.    schedule 28.01.2013
comment
@n.m: байты без чередования передаются в стороннюю библиотеку. Я мог бы возможно изменить стороннюю библиотеку, чтобы она индексировала по-другому, но это было бы последним средством, которое не помогло.   -  person Anton    schedule 28.01.2013
comment
Вам не нужно изменять его интерфейс. Что-то вроде short a=((short*)mixed)[i]; array1[i] = a&0xFF; array2[i] = a>>8;.   -  person n. 1.8e9-where's-my-share m.    schedule 28.01.2013
comment
Вы смотрели API фреймворка Accelerate? Вы, несомненно, найдете там то, что вам нужно.   -  person P i    schedule 07.01.2016
comment
Я думаю, вы можете использовать vunzp.8 для NEON части вашей программы. Похоже, Стивен дал это вам ниже. См. также Кодирование для NEON. Часть 5. Перестановка векторов.   -  person jww    schedule 04.12.2017


Ответы (6)


Внезапно я не знаю библиотечной функции для устранения чередования двухканальных байтовых данных. Однако стоит подать отчет об ошибке в Apple, чтобы запросить такую ​​​​функцию.

Тем временем довольно легко векторизовать такую ​​функцию, используя встроенные функции NEON или SSE. В частности, на ARM вы захотите использовать vld1q_u8 для загрузки вектора из каждого исходного массива, vuzpq_u8 для их обратного чередования и vst1q_u8 для сохранения результирующих векторов; вот грубый набросок, который я не тестировал и даже не пытался построить, но он должен проиллюстрировать общую идею. Определенно возможны и более сложные реализации (в частности, NEON может загружать/сохранять два 16-байтных регистра в одной инструкции, чего компилятор может не делать с этим, и может потребоваться некоторый объем конвейерной обработки и/или развертывания). полезно в зависимости от того, как долго ваши буферы):

#if defined __ARM_NEON__
#   include <arm_neon.h>
#endif
#include <stdint.h>
#include <stddef.h>

void deinterleave(uint8_t *mixed, uint8_t *array1, uint8_t *array2, size_t mixedLength) {
#if defined __ARM_NEON__
    size_t vectors = mixedLength / 32;
    mixedLength %= 32;
    while (vectors --> 0) {
        const uint8x16_t src0 = vld1q_u8(mixed);
        const uint8x16_t src1 = vld1q_u8(mixed + 16);
        const uint8x16x2_t dst = vuzpq_u8(src0, src1);
        vst1q_u8(array1, dst.val[0]);
        vst1q_u8(array2, dst.val[1]);
        mixed += 32;
        array1 += 16;
        array2 += 16;
    }
#endif
    for (size_t i=0; i<mixedLength/2; ++i) {
        array1[i] = mixed[2*i];
        array2[i] = mixed[2*i + 1];
    }
}
person Stephen Canon    schedule 28.01.2013
comment
Даже если бы речь шла о типах float и int, у меня было бы то же беспокойство, что и у OP в этом вопросе, когда я использовал векторные инструкции float для перетасовки int, умноженные на столько платформ, сколько предназначена для платформы Accelerate. Ответ тонкий только для архитектуры x86. stackoverflow.com/questions/4996384/ - person Pascal Cuoq; 28.01.2013
comment
@PascalCuoq: на самом деле это не проблема; данные будут обрабатываться полностью как FP, поэтому не будет штрафов за пересечение домена. Однако это спорный вопрос. - person Stephen Canon; 28.01.2013
comment
Вау, встроенные функции NEON смехотворно быстрые. Я использую vld2q_u8 и vst1q_u8 без vuzpq_u8, и он полыхает. - person Anton; 29.01.2013
comment
@Anton: FWIW, использование vuzpq_u8 на некоторых процессорах будет еще быстрее. - person Stephen Canon; 29.01.2013
comment
Я знаю, что этот вопрос устарел, но я хотел бы подтвердить, что де-чередование быстрее с vuzpq_u8 , чем vld2q_u8 на iPhone X примерно в 1,5 раза (не научно). - person user2888798; 12.11.2019

Я только слегка протестировал это, но мне показалось, что он как минимум в два раза быстрее вашей версии:

typedef union {
uint16_t wide;
struct { uint8_t top; uint8_t bottom; } narrow;
} my_union;

uint16_t *source = (uint16_t *)mixed;
for (int i = 0; i < mixedLength/2; i++)
{
    my_union cursor;
    cursor.wide = source[i];
    array1[i] = cursor.narrow.top;
    array2[i] = cursor.narrow.bottom;
}

Обратите внимание, что я не был осторожен с упаковкой структуры, но в данном случае для этой архитектуры это не проблема. Заметьте также, что кто-то может пожаловаться на мой выбор имен top и bottom; Я предполагаю, что вы знаете, какая половина целых чисел вам нужна.

person Community    schedule 28.01.2013
comment
Я смущен, почему эта версия будет быстрее. Это, конечно, запутывает происходящее. - person R.. GitHub STOP HELPING ICE; 28.01.2013
comment
Это умное использование объединения и хороший способ уменьшить количество операций на итерацию... Мне это нравится. - person Anton; 28.01.2013
comment
Зачем нужен профсоюз? Простое использование структуры дает здесь точно такой же эффект. - person Mecki; 28.01.2013
comment
Несмотря на то, что ваша версия не является endian безопасной. Результаты в array1 и array2 будут зависеть от порядка байтов платформы. - person Mecki; 28.01.2013
comment
@mecki, как указано в ответе. Я предполагаю, что спрашивающий знает, какой байт какой. - person ; 28.01.2013
comment
@mecki также союз для ясности: он документирует тот факт, что одна и та же память будет использоваться как две разные вещи. - person ; 28.01.2013
comment
Спасибо, @GrahamLee. Хотел бы я принять это как ответ, так как я использую его, если встроенные функции NEON недоступны. - person Anton; 29.01.2013

Хорошо, вот ваш оригинальный метод:

static void simpleDeint (
    uint8_t * array1, uint8_t * array2, uint8_t * mixed, int mixedLength
) {
    int i, j;
    int mixedLength_2 = mixedLength / 2;
    for (i = 0, j = 0; i < mixedLength_2; i++, j += 2)
    {
        array1[i] = mixed[j];
        array2[i] = mixed[j+1];
    }
}

С 10 миллионами записей и -O3 (компилятор должен оптимизировать для максимальной скорости), я могу запускать это 154 раза в секунду на своем Mac.

Вот мое первое предложение:

static void structDeint (
    uint8_t * array1, uint8_t * array2, uint8_t * mixed, int mixedLength
) {
    int i;
    int len;
    uint8_t * array1Ptr = (uint8_t *)array1;
    uint8_t * array2Ptr = (uint8_t *)array2;
    struct {
        uint8_t byte1;
        uint8_t byte2;
    } * tb = (void *)mixed;

    len = mixedLength / 2;
    for (i = 0; i < len; i++) {
      *(array1Ptr++) = tb->byte1;
      *(array2Ptr++) = tb->byte2;
      tb++;
    }
}

Тот же подсчет и оптимизация, что и раньше, я получаю 193 запуска в секунду.

Теперь предложение от Грэма Ли:

static void unionDeint (
    uint8_t * array1, uint8_t * array2, uint8_t * mixed, int mixedLength
) {
    union my_union {
        uint16_t wide;
        struct { uint8_t top; uint8_t bottom; } narrow;
    };

    uint16_t * source = (uint16_t *)mixed;
    for (int i = 0; i < mixedLength/2; i++) {
        union my_union cursor;
        cursor.wide = source[i];
        array1[i] = cursor.narrow.top;
        array2[i] = cursor.narrow.bottom;
    }
}

Та же настройка, что и раньше, 198 запусков в секунду (ПРИМЕЧАНИЕ. Этот метод не является безопасным по порядку байтов, результат зависит от порядка следования байтов ЦП. В вашем случае массив1 и массив2, вероятно, поменяны местами, поскольку ARM имеет обратный порядок байтов, поэтому вам придется поменять их местами в коде. ).

Вот мой лучший на данный момент:

static void uint32Deint (
    uint8_t * array1, uint8_t * array2, uint8_t * mixed, int mixedLength
) {
    int i;
    int count;
    uint32_t * fourBytes = (void *)mixed;
    uint8_t * array1Ptr = (uint8_t *)array1;
    uint8_t * array2Ptr = (uint8_t *)array2;


    count = mixedLength / 4;
    for (i = 0; i < count; i++) {
        uint32_t temp = *(fourBytes++);

#if __LITTLE_ENDIAN__
        *(array1Ptr++) = (uint8_t)(temp & 0xFF);
        temp >>= 8;
        *(array2Ptr++) = (uint8_t)(temp & 0xFF);
        temp >>= 8;
        *(array1Ptr++) = (uint8_t)(temp & 0xFF);
        temp >>= 8;
        *(array2Ptr++) = tb->byte2;

#else
        *(array1Ptr++) = (uint8_t)(temp >> 24);
        *(array2Ptr++) = (uint8_t)((temp >> 16) & 0xFF);
        *(array1Ptr++) = (uint8_t)((temp >>  8) & 0xFF);
        *(array2Ptr++) = (uint8_t)(temp & 0xFF);
#endif
    }
    // Either it is a multiple of 4 or a multiple of 2.
    // If it is a multiple of 2, 2 bytes are left over.
    if (count * 4 != mixedLength) {
        *(array1Ptr) = mixed[mixedLength - 2];
        *(array2Ptr) = mixed[mixedLength - 1];
    }
}

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

person Mecki    schedule 28.01.2013

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

Идея такова:

  1. Чтение целого 32-битного целого числа из mixed. Вы получите «a1b2».

  2. Поверните младшие 16 бит на 8 бит, чтобы получить «1ab2» (мы используем прямой порядок байтов, так как это значение по умолчанию в ARM и, следовательно, Apple A#, поэтому первые два байта являются младшими).

  3. Поверните весь 32-битный регистр вправо (я думаю, это правильно...) на 8 бит, чтобы получить «21ab».

  4. Поверните младшие 16 бит на 8 бит, чтобы получить «12ab».

  5. Запишите младшие 8 бит в array2.

  6. Поверните весь 32-битный регистр на 16 бит.

  7. Запишите младшие 8 бит в array1

  8. Сдвинуть array1 на 16 бит, array2 на 16 бит и mixed на 32 бита.

  9. Повторение.

Мы обменяли 2 чтения памяти (при условии, что мы используем версию Грэма или эквивалентную) и 4 памяти с одним чтением памяти, двумя операциями записи памяти и 4 операциями регистра. Хотя количество операций увеличилось с 6 до 7, операции с регистрами выполняются быстрее, чем операции с памятью, поэтому они более эффективны. Кроме того, поскольку мы читаем mixed по 32 бита за раз вместо 16, мы вдвое сократили управление итерациями.

PS: Теоретически это можно сделать и для 64-битной архитектуры, но выполнение всех этих поворотов для 'a1b2c3d4' сведет вас с ума.

person Idan Arye    schedule 28.01.2013
comment
Если вы собираетесь использовать ассемблер, почему бы вам не использовать SIMD-инструкции, которые были бы намного быстрее? - person Eric Postpischil; 28.01.2013
comment
В основном потому, что я никогда не изучал ассемблер в достаточной степени, чтобы использовать их. - person Idan Arye; 28.01.2013

Для x86 SSE вам нужны инструкции pack и punpck. Примеры использования AVX для удобства неразрушающих инструкций с 3 операндами. (Не используются инструкции AVX2 шириной 256 бит, потому что инструкции упаковки/распаковки 256 бит выполняют две распаковки 128 бит в нижней и верхней полосах 128 бит, поэтому вам потребуется перетасовка, чтобы получить вещи в правильном окончательном порядке.)

Внутренняя версия следующего будет работать так же. Инструкции Asm короче для быстрого написания ответа.

Чередовать: abcd и 1234 -> a1b2c3d4:

# loop body:
vmovdqu    (%rax), %xmm0  # load the sources
vmovdqu    (%rbx), %xmm1
vpunpcklbw %xmm0, %xmm1, %xmm2  # low  halves -> 128b reg
vpunpckhbw %xmm0, %xmm2, %xmm3  # high halves -> 128b reg
vmovdqu    %xmm2, (%rdi)   # store the results
vmovdqu    %xmm3, 16(%rdi)
# blah blah some loop structure.

`punpcklbw` interleaves the bytes in the low 64 of the two source `xmm` registers.  There are `..wd` (word->dword), and dword->qword versions which would be useful for 16 or 32bit elements.

Отменить чередование: a1b2c3d4 -> abcd и 1234

#outside the loop
vpcmpeqb    %xmm5, %xmm5   # set to all-1s
vpsrlw     $8, %xmm5, %xmm5   # every 16b word has low 8b = 0xFF, high 8b = 0.

# loop body
vmovdqu    (%rsi), %xmm2     # load two src chunks
vmovdqu    16(%rsi), %xmm3
vpand      %xmm2, %xmm5, %xmm0  # mask to leave only the odd bytes
vpand      %xmm3, %xmm5, %xmm1
vpackuswb  %xmm0, %xmm1, %xmm4
vmovdqu    %xmm4, (%rax)    # store 16B of a[]
vpsrlw     $8, %xmm2, %xmm6     # even bytes -> odd bytes
vpsrlw     $8, %xmm3, %xmm7
vpackuswb  %xmm6, %xmm7, %xmm4
vmovdqu    %xmm4, (%rbx)

Это, конечно, может использовать намного меньше регистров. Я избегал повторного использования регистров для удобства чтения, а не для производительности. Переименование аппаратного регистра делает повторное использование не проблематичным, если вы начинаете с чего-то, что не зависит от предыдущего значения. (например, movd, а не movss или pinsrd.)

Обратное чередование требует гораздо больше работы, потому что инструкции pack выполняют насыщение со знаком или без знака, поэтому сначала необходимо обнулить верхние 8b каждого 16-битного элемента.

Альтернативой может быть использование pshufb для упаковки нечетных или четных слов одного исходного регистра в младшие 64 регистра. Однако, кроме VPPERM набора инструкций AMD XOP, не существует тасования, которое может выбирать байты из 2 регистров одновременно (например, так любимый Altivec vperm). Таким образом, используя только SSE/AVX, вам потребуется 2 перетасовки на каждые 128 байт чередующихся данных. И поскольку использование порта хранилища может быть узким местом, punpck для объединения двух 64-битных фрагментов a в один регистр для настройки хранилища размером 128 байт.

С AMD XOP обратное чередование будет состоять из 2x128b загрузки, 2 VPPERM и 2x128b сохранения.

person Peter Cordes    schedule 05.07.2015

  1. преждевременная оптимизация это плохо

  2. ваш компилятор, вероятно, лучше оптимизирует, чем вы.

Тем не менее, есть некоторые вещи, которые вы можете сделать, чтобы помочь компилятору, потому что у вас есть семантическое знание ваших данных, которое не может быть у компилятора:

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

  2. развернуть петли - заглянуть в "Устройство Даффа".

FWIW, я создал две версии вашего цикла копирования, одна из которых очень похожа на вашу, а вторая использует то, что большинство считает «оптимальным» (хотя и все еще простым) C-кодом:

void test1(byte *p, byte *p1, byte *p2, int n)
{
    int i, j;
    for (i = 0, j = 0; i < n / 2; i++, j += 2) {
        p1[i] = p[j];
        p2[i] = p[j + 1];
    }
}

void test2(byte *p, byte *p1, byte *p2, int n)
{
    while (n) {
        *p1++ = *p++;
        *p2++ = *p++;
        n--; n--;
    }
}

С gcc -O3 -S на Intel x86 они оба создавали почти идентичный ассемблерный код. Вот внутренние петли:

LBB1_2:
    movb    -1(%rdi), %al
    movb    %al, (%rsi)
    movb    (%rdi), %al
    movb    %al, (%rdx)
    incq    %rsi
    addq    $2, %rdi
    incq    %rdx
    decq    %rcx
    jne LBB1_2

и

LBB2_2:
    movb    -1(%rdi), %al
    movb    %al, (%rsi)
    movb    (%rdi), %al
    movb    %al, (%rdx)
    incq    %rsi
    addq    $2, %rdi
    incq    %rdx
    addl    $-2, %ecx
    jne LBB2_2

Оба имеют одинаковое количество инструкций, разница объясняется исключительно тем, что первая версия считает до n / 2, а вторая считает до нуля.

ИЗМЕНИТЬ вот лучшая версия:

/* non-portable - assumes little endian */
void test3(byte *p, byte *p1, byte *p2, int n)
{
    ushort *ps = (ushort *)p;

    n /= 2;
    while (n) {
        ushort n = *ps++;
        *p1++ = n;
        *p2++ = n >> 8;
    }
}

в результате чего:

LBB3_2:
    movzwl  (%rdi), %ecx
    movb    %cl, (%rsi)
    movb    %ch, (%rdx)  # NOREX
    addq    $2, %rdi
    incq    %rsi
    incq    %rdx
    decq    %rax
    jne LBB3_2

что на одну инструкцию меньше, поскольку использует немедленный доступ к %cl и %ch.

person Alnitak    schedule 28.01.2013
comment
Теоретически я согласен — пусть компилятор оптимизирует ваш код за вас. Это один из тех ‹ 1% низкоуровневых случаев, когда даже небольшое сокращение времени вычислений может иметь значительное общее влияние на производительность системы. - person Anton; 28.01.2013
comment
работает недостаточно быстро — это признак того, что оптимизация не преждевременна. - person ; 28.01.2013