Алгоритм подсчета столбцов AVX2 по каждому битовому столбцу отдельно

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

Я пытаюсь получить общее количество битов набора для каждого столбца во всем задании PDF (все страницы).

Данные после копирования сохраняются в MemoryMappedFile без резервного файла (в памяти).

Размеры страницы PDF составляют 13952 x 15125 пикселей. Общий размер результирующих скопированных данных можно рассчитать, умножив длину (высоту) PDF в пикселях на ширину в байтах. Скопированные данные: 1 bit == 1 pixel. Таким образом, размер скопированной страницы в байтах равен (13952 / 8) * 15125.

Обратите внимание, что ширина всегда кратна 64 bits.

Мне придется подсчитать установленные биты для каждого столбца на каждой странице PDF (а это могут быть десятки тысяч страниц) после копирования.

Сначала я подошел к проблеме с базовым решением, состоящим в простом переборе каждого байта, подсчете количества установленных битов и размещении результатов в vector. С тех пор я сократил алгоритм до того, что показано ниже. Я увеличил время выполнения с ~ 350 мс до ~ 120 мс.

static void count_dots( )
{
    using namespace diag;
    using namespace std::chrono;

    std::vector<std::size_t> dot_counts( 13952, 0 );
    uint64_t* ptr_dot_counts{ dot_counts.data( ) };

    std::vector<uint64_t> ripped_pdf_data( 3297250, 0xFFFFFFFFFFFFFFFFUL );
    const uint64_t* ptr_data{ ripped_pdf_data.data( ) };

    std::size_t line_count{ 0 };
    std::size_t counter{ ripped_pdf_data.size( ) };

    stopwatch sw;
    sw.start( );

    while( counter > 0 )
    {
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 7 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 6 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 5 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 4 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 3 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 2 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 1 ) & 0x0000000000000001UL ) >> 0;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0100000000000000UL ) >> 56;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0001000000000000UL ) >> 48;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0000010000000000UL ) >> 40;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0000000100000000UL ) >> 32;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0000000001000000UL ) >> 24;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0000000000010000UL ) >> 16;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0000000000000100UL ) >> 8;
        *ptr_dot_counts++ += ( ( *ptr_data >> 0 ) & 0x0000000000000001UL ) >> 0;

        ++ptr_data;
        --counter;
        if( ++line_count >= 218 )
        {
            ptr_dot_counts = dot_counts.data( );
            line_count = 0;
        }
    }   

    sw.stop( );
    std::cout << sw.elapsed<milliseconds>( ) << "ms\n";
}

К сожалению, это все еще добавит много дополнительного времени обработки, что неприемлемо.

Приведенный выше код уродлив и не выиграет ни одного конкурса красоты, но он помог сократить время выполнения. Начиная с оригинальной версии, которую я написал, я сделал следующее:

  • Используйте pointers вместо indexers
  • Обрабатывайте данные порциями по uint64 вместо uint8
  • Вручную разверните цикл for для обхода каждого bit в каждом byte из uint64
  • Используйте конечный bit shift вместо __popcnt64 для подсчета набора bit после маскирования

Для этого теста я генерирую фальшивые скопированные данные, где каждому bit присвоено значение 1. dot_counts vector должно содержать 15125 для каждого element после завершения теста.

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

  • ЦП целевой машины: Xeon E5-2680 v4 - Intel
  • Компилятор: MSVC++ 14.23
  • OS: Windows 10
  • Версия С++: C++17
  • Флаги компилятора: /O2 /arch:AVX2

Очень похожий вопрос был задан примерно 8 лет назад: of-ints-on-sandy-bridge">Как быстро посчитать биты в отдельные ячейки в серии целых чисел на Sandy Bridge?

(Примечание редактора: возможно, вы пропустили Подсчитайте каждую битовую позицию отдельно по многим 64-битным битовым маскам с AVX, но не AVX2, у которого есть несколько более поздних более быстрых ответов, по крайней мере, для перехода вниз по столбцу, а не по строке в непрерывной памяти. , Может быть, вы можете использовать 1 или 2 строки кэша в ширину вниз по столбцу, чтобы вы могли поддерживать свои счетчики в SIMD-регистрах горячими.)

Когда я сравниваю то, что у меня есть до сих пор, с принятым ответом, я довольно близок. Я уже обрабатывал кусками uint64 вместо uint8. Мне просто интересно, могу ли я сделать что-то еще, будь то встроенные функции, сборка или что-то простое, например изменение используемых структур данных.


person WBuck    schedule 21.10.2019    source источник
comment
Чуть позже я, возможно, углублюсь, но после быстрого просмотра вы сможете достаточно увеличить скорость, просто кэшируя побитовые результаты. Вы повторяете сдвиг вправо 8 раз для каждого байта, что совершенно не нужно.   -  person Pickle Rick    schedule 21.10.2019
comment
@PickleRick Когда вы говорите о кэшировании побитовых результатов, вы имеете в виду кэширование результата, скажем, *ptr_data >> 7 и его повторное использование?   -  person WBuck    schedule 21.10.2019
comment
Да, точно. Сохраните сдвинутые результаты ptr_data для всех 8 байтов, чтобы их не приходилось пересчитывать (8 * 8 = 64) раз на каждой итерации.   -  person Pickle Rick    schedule 21.10.2019
comment
Просто для уточнения: ваши изображения хранятся по строкам? Всегда ли размер один и тот же, или вы можете гарантировать только то, что ширина кратна 64? Вам нужен результат как вектор uint64 или вектор uint16 тоже подойдет? И можете ли вы сделать какие-либо предположения о выравнивании?   -  person chtz    schedule 21.10.2019
comment
@PickleRick хорошая идея, я проверю   -  person WBuck    schedule 21.10.2019
comment
Дайте мне знать, как это происходит, если это все еще недостаточно быстро, я еще раз посмотрю немного позже сегодня, если вы все еще не получили приемлемого ответа.   -  person Pickle Rick    schedule 21.10.2019
comment
@chtz данные хранятся в блоке памяти contiguous (массив 1D ), поэтому row-major. Длина (высота) является динамической, но всегда будет кратной 5. В настоящее время я использую uint64 для хранения счетчиков, потому что я собираю общее количество точек для всей работы (все страницы PDF). Хотя это поведение можно изменить. Я мог бы просто сохранить общее количество для каждой страницы, а затем объединить полученные vectors в конце   -  person WBuck    schedule 21.10.2019
comment
Это: подсчет установленных битов для каждого столбца на каждой странице заставил меня предположить, что вас в первую очередь интересует количество на странице. Но вас (только?) интересует общее количество (для каждого столбца)?   -  person chtz    schedule 21.10.2019
comment
@chtz правильно, я отредактирую вопрос, чтобы уточнить это.   -  person WBuck    schedule 21.10.2019
comment
Я должен добавить, что правый сдвиг 0 на самом деле ничего не сделает, поэтому эти 8 можно удалить. Хотя оптимизация компилятора должна уловить это, так что это может даже не иметь значения. Я все еще пользуюсь мобильным телефоном, но, как я уже говорил, дайте мне знать, если этого быстрого изменения недостаточно, и я вернусь к вам чуть позже, когда доберусь до компьютера.   -  person Pickle Rick    schedule 21.10.2019
comment
@PickleRick Хорошо, поэтому я сохранил результаты bit shift, но без изменений. Я подозреваю, что компилятор увидел, что я делаю, и cached результат для меня в переменной. Я вставлю код в Compiler Explorer и посмотрю, что делает компилятор.   -  person WBuck    schedule 21.10.2019
comment
Сборка была бы полезна, спасибо. Между оптимизацией компилятора и побитовыми операторами с такой низкой задержкой это не слишком удивительно, но я надеялся, что этого будет достаточно. Я буду за компьютером в течение следующего часа и поищу тебя поглубже.   -  person Pickle Rick    schedule 21.10.2019
comment
( ( *ptr_data >> 7 ) & 0x0100000000000000UL ) >> 56 может быть !!(*ptr_data & 0x80'00'00'00'00'00'00'00UL), а ( ( *ptr_data >> 7 ) & 0x0001000000000000UL ) >> 48 может быть !!(*ptr_data & 0x40'00'00'00'00'00'00'00UL) и так далее.   -  person Jarod42    schedule 21.10.2019
comment
И чтобы избежать возможных псевдонимов, вы можете сохранить *ptr_data в локальной переменной.   -  person Jarod42    schedule 21.10.2019
comment
@ Джарод42 Интересно. Раньше я пытался implicitly преобразовать в bool, но мне все еще было bit-shifting. Сейчас попробую ваши предложения.   -  person WBuck    schedule 21.10.2019
comment
!! кажется не очень хорошим, так как msvc создает прыжок, но я хочу сделать только одну смену: (*ptr_data & 0x80'00'00'00'00'00'00'00UL) >> 63 или даже (*ptr_data >> 63) & 0x01   -  person Jarod42    schedule 21.10.2019
comment
Связанный с этим вопрос здесь с некоторыми тестами, следуя идее Питера Кордеса. Он работает с 32-битными масками, но должен быть улучшен для 64-битных масок.   -  person edrezen    schedule 14.01.2020


Ответы (1)


Это можно сделать с помощью AVX2, как помечено.

Чтобы все работало правильно, я рекомендую vector<uint16_t> для подсчета. Добавление к подсчетам — самая большая проблема, и чем больше нам нужно расширяться, тем больше проблема. uint16_t достаточно для подсчета одной страницы, поэтому вы можете подсчитывать одну страницу за раз и добавлять счетчики в набор более широких счетчиков для итогов. Это некоторые накладные расходы, но гораздо меньшие, чем необходимость расширения в основном цикле.

Порядок подсчета с обратным порядком байтов очень раздражает, внося еще больше перетасовок, чтобы все было правильно. Поэтому я рекомендую сделать это неправильно и изменить порядок подсчетов позже (может быть, во время суммирования их в итоги?). Порядок «сдвиг вправо сначала на 7, затем на 6, затем на 5» может поддерживаться бесплатно, потому что мы можем выбирать количество сдвигов для 64-битных блоков любым удобным для нас способом. Таким образом, в приведенном ниже коде фактический порядок подсчета таков:

  • Бит 7 младшего значащего байта,
  • Бит 7 второго байта
  • ...
  • Бит 7 старшего байта,
  • Бит 6 младшего значащего байта,
  • ...

Таким образом, каждая группа из 8 перевернута. (по крайней мере, это то, что я собирался сделать, AVX2 распаковывает смущают)

Код (не проверен):

while( counter > 0 )
{
    __m256i data = _mm256_set1_epi64x(*ptr_data);        
    __m256i data1 = _mm256_srlv_epi64(data, _mm256_set_epi64x(4, 6, 5, 7));
    __m256i data2 = _mm256_srlv_epi64(data, _mm256_set_epi64x(0, 2, 1, 3));
    data1 = _mm256_and_si256(data1, _mm256_set1_epi8(1));
    data2 = _mm256_and_si256(data2, _mm256_set1_epi8(1));

    __m256i zero = _mm256_setzero_si256();

    __m256i c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[0]);
    c = _mm256_add_epi16(_mm256_unpacklo_epi8(data1, zero), c);
    _mm256_storeu_si256((__m256i*)&ptr_dot_counts[0], c);

    c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[16]);
    c = _mm256_add_epi16(_mm256_unpackhi_epi8(data1, zero), c);
    _mm256_storeu_si256((__m256i*)&ptr_dot_counts[16], c);

    c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[32]);
    c = _mm256_add_epi16(_mm256_unpacklo_epi8(data2, zero), c);
    _mm256_storeu_si256((__m256i*)&ptr_dot_counts[32], c);

    c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[48]);
    c = _mm256_add_epi16(_mm256_unpackhi_epi8(data2, zero), c);
    _mm256_storeu_si256((__m256i*)&ptr_dot_counts[48], c);

    ptr_dot_counts += 64;
    ++ptr_data;
    --counter;
    if( ++line_count >= 218 )
    {
        ptr_dot_counts = dot_counts.data( );
        line_count = 0;
    }
}

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

Используемые внутренние компоненты Somme:

  • _mm256_set1_epi64x, копирует один int64_t во все 4 64-битных элемента вектора. Также подходит для uint64_t.
  • _mm256_set_epi64x превращает 4 64-битных значения в вектор.
  • _mm256_srlv_epi64, логический сдвиг вправо, с переменным счетчиком (может быть разное количество для каждого элемента).
  • _mm256_and_si256, просто побитовое И.
  • Кроме того, _mm256_add_epi16 работает с 16-битными элементами.
  • _mm256_unpacklo_epi8 и _mm256_unpackhi_epi8, вероятно, лучше всего объясняется диаграммами на этой странице.

Можно суммировать «вертикально», используя один uint64_t для хранения всех 0-х битов 64 отдельных сумм, другой uint64_t для хранения всех 1-х битов сумм и т. д. Сложение можно выполнить, эмулируя полные сумматоры (компонент схемы ) с побитовой арифметикой. Затем вместо того, чтобы добавлять только 0 или 1 к счетчикам, все большие числа добавляются сразу.

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

Пример (не проверено):

size_t y;
// sum 7 rows at once
for (y = 0; (y + 6) < 15125; y += 7) {
    ptr_dot_counts = dot_counts.data( );
    ptr_data = ripped_pdf_data.data( ) + y * 218;
    for (size_t x = 0; x < 218; x++) {
        uint64_t dataA = ptr_data[0];
        uint64_t dataB = ptr_data[218];
        uint64_t dataC = ptr_data[218 * 2];
        uint64_t dataD = ptr_data[218 * 3];
        uint64_t dataE = ptr_data[218 * 4];
        uint64_t dataF = ptr_data[218 * 5];
        uint64_t dataG = ptr_data[218 * 6];
        // vertical sums, 7 bits to 3
        uint64_t abc0 = (dataA ^ dataB) ^ dataC;
        uint64_t abc1 = (dataA ^ dataB) & dataC | (dataA & dataB);
        uint64_t def0 = (dataD ^ dataE) ^ dataF;
        uint64_t def1 = (dataD ^ dataE) & dataF | (dataD & dataE);
        uint64_t bit0 = (abc0 ^ def0) ^ dataG;
        uint64_t c1   = (abc0 ^ def0) & dataG | (abc0 & def0);
        uint64_t bit1 = (abc1 ^ def1) ^ c1;
        uint64_t bit2 = (abc1 ^ def1) & c1 | (abc1 & def1);
        // add vertical sums to column counts
        __m256i bit0v = _mm256_set1_epi64x(bit0);
        __m256i data01 = _mm256_srlv_epi64(bit0v, _mm256_set_epi64x(4, 6, 5, 7));
        __m256i data02 = _mm256_srlv_epi64(bit0v, _mm256_set_epi64x(0, 2, 1, 3));
        data01 = _mm256_and_si256(data01, _mm256_set1_epi8(1));
        data02 = _mm256_and_si256(data02, _mm256_set1_epi8(1));
        __m256i bit1v = _mm256_set1_epi64x(bit1);
        __m256i data11 = _mm256_srlv_epi64(bit1v, _mm256_set_epi64x(4, 6, 5, 7));
        __m256i data12 = _mm256_srlv_epi64(bit1v, _mm256_set_epi64x(0, 2, 1, 3));
        data11 = _mm256_and_si256(data11, _mm256_set1_epi8(1));
        data12 = _mm256_and_si256(data12, _mm256_set1_epi8(1));
        data11 = _mm256_add_epi8(data11, data11);
        data12 = _mm256_add_epi8(data12, data12);
        __m256i bit2v = _mm256_set1_epi64x(bit2);
        __m256i data21 = _mm256_srlv_epi64(bit2v, _mm256_set_epi64x(4, 6, 5, 7));
        __m256i data22 = _mm256_srlv_epi64(bit2v, _mm256_set_epi64x(0, 2, 1, 3));
        data21 = _mm256_and_si256(data21, _mm256_set1_epi8(1));
        data22 = _mm256_and_si256(data22, _mm256_set1_epi8(1));
        data21 = _mm256_slli_epi16(data21, 2);
        data22 = _mm256_slli_epi16(data22, 2);
        __m256i data1 = _mm256_add_epi8(_mm256_add_epi8(data01, data11), data21);
        __m256i data2 = _mm256_add_epi8(_mm256_add_epi8(data02, data12), data22);

        __m256i zero = _mm256_setzero_si256();

        __m256i c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[0]);
        c = _mm256_add_epi16(_mm256_unpacklo_epi8(data1, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[0], c);

        c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[16]);
        c = _mm256_add_epi16(_mm256_unpackhi_epi8(data1, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[16], c);

        c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[32]);
        c = _mm256_add_epi16(_mm256_unpacklo_epi8(data2, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[32], c);

        c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[48]);
        c = _mm256_add_epi16(_mm256_unpackhi_epi8(data2, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[48], c);


        ptr_dot_counts += 64;
        ++ptr_data;
    }
}
// leftover rows
for (; y < 15125; y++) {
    ptr_dot_counts = dot_counts.data( );
    ptr_data = ripped_pdf_data.data( ) + y * 218;
    for (size_t x = 0; x < 218; x++) {
        __m256i data = _mm256_set1_epi64x(*ptr_data);
        __m256i data1 = _mm256_srlv_epi64(data, _mm256_set_epi64x(4, 6, 5, 7));
        __m256i data2 = _mm256_srlv_epi64(data, _mm256_set_epi64x(0, 2, 1, 3));
        data1 = _mm256_and_si256(data1, _mm256_set1_epi8(1));
        data2 = _mm256_and_si256(data2, _mm256_set1_epi8(1));

        __m256i zero = _mm256_setzero_si256();

        __m256i c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[0]);
        c = _mm256_add_epi16(_mm256_unpacklo_epi8(data1, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[0], c);

        c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[16]);
        c = _mm256_add_epi16(_mm256_unpackhi_epi8(data1, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[16], c);

        c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[32]);
        c = _mm256_add_epi16(_mm256_unpacklo_epi8(data2, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[32], c);

        c = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[48]);
        c = _mm256_add_epi16(_mm256_unpackhi_epi8(data2, zero), c);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[48], c);


        ptr_dot_counts += 64;
        ++ptr_data;
    }
}

Вторым лучшим до сих пор был более простой подход, больше похожий на первую версию, за исключением одновременного запуска yloopLen строк, чтобы воспользоваться преимуществами быстрых 8-битных сумм:

size_t yloopLen = 32;
size_t yblock = yloopLen * 1;
size_t yy;
for (yy = 0; yy < 15125; yy += yblock) {
    for (size_t x = 0; x < 218; x++) {
        ptr_data = ripped_pdf_data.data() + x;
        ptr_dot_counts = dot_counts.data() + x * 64;
        __m256i zero = _mm256_setzero_si256();

        __m256i c1 = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[0]);
        __m256i c2 = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[16]);
        __m256i c3 = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[32]);
        __m256i c4 = _mm256_loadu_si256((__m256i*)&ptr_dot_counts[48]);

        size_t end = std::min(yy + yblock, size_t(15125));
        size_t y;
        for (y = yy; y < end; y += yloopLen) {
            size_t len = std::min(size_t(yloopLen), end - y);
            __m256i count1 = zero;
            __m256i count2 = zero;

            for (size_t t = 0; t < len; t++) {
                __m256i data = _mm256_set1_epi64x(ptr_data[(y + t) * 218]);
                __m256i data1 = _mm256_srlv_epi64(data, _mm256_set_epi64x(4, 6, 5, 7));
                __m256i data2 = _mm256_srlv_epi64(data, _mm256_set_epi64x(0, 2, 1, 3));
                data1 = _mm256_and_si256(data1, _mm256_set1_epi8(1));
                data2 = _mm256_and_si256(data2, _mm256_set1_epi8(1));
                count1 = _mm256_add_epi8(count1, data1);
                count2 = _mm256_add_epi8(count2, data2);
            }

            c1 = _mm256_add_epi16(_mm256_unpacklo_epi8(count1, zero), c1);
            c2 = _mm256_add_epi16(_mm256_unpackhi_epi8(count1, zero), c2);
            c3 = _mm256_add_epi16(_mm256_unpacklo_epi8(count2, zero), c3);
            c4 = _mm256_add_epi16(_mm256_unpackhi_epi8(count2, zero), c4);
        }

        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[0], c1);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[16], c2);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[32], c3);
        _mm256_storeu_si256((__m256i*)&ptr_dot_counts[48], c4);
    }
}

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

person harold    schedule 21.10.2019
comment
Я бы также рассмотрел возможность выполнения самых внутренних дополнений на уровне байтов (или даже иерархически 2-битных, 4-битных, 8-битных, 16-битных). И я думаю, что одна из главных вещей, которую следует учитывать, — это оптимальное использование регистров и кеша. - person chtz; 21.10.2019
comment
Я пытаюсь мысленно распаковать приведенный выше код. В настоящее время, когда я запускаю код как есть, vector<uint16_t> dot_counts( 13952, 0 ) показывает неверные подсчеты. Первые элементы 64 отображают количество «20450», а остальные элементы показывают «0». Я не уверен, является ли это ошибкой (как вы сказали, непроверенный код) или, в равной степени, возможно, я не понимаю, как должны обновляться counts. Сейчас я просто просматриваю фрагмент кода, чтобы попытаться понять его. - person WBuck; 21.10.2019
comment
Я забыл переместить указатель, но, возможно, больше неправильно. Если/когда это перестанет быть неправильным, возможно, я добавлю некоторые предложения @chtz (или он может просто написать ответ) - person harold; 21.10.2019
comment
@harold вау, это впечатляет. Время выполнения сократилось в среднем с ~110 мс до ~10 мс. Счетчики dot_counts верны после перемещения указателя. Теперь мне нужно выполнить свою работу и понять, что делает код построчно, прежде чем реализовать его. Спасибо за ваше время и усилия. - person WBuck; 21.10.2019
comment
@WBuck Я добавил другую (более странную) версию. Если хотите, я могу добавить несколько ссылок на то, как работают инструкции, которые я использовал. - person harold; 21.10.2019
comment
@harold Да, мне бы понравились ссылки. Я также должен попробовать эту новую версию. - person WBuck; 21.10.2019
comment
Версия 2 сократила время выполнения с ~10 мс (версия 1) до ~5 мс. Вы задали мне много домашней работы. - person WBuck; 21.10.2019
comment
@WBuck и Гарольд: мой ответ на Подсчитайте каждую битовую позицию отдельно по многим 64-битным битовым маскам, с AVX, но не с AVX2, имеет постепенный расширение версии этого с использованием скаляра uint64_t. Я не тратил время на ручную векторизацию; вы хотели бы перейти вниз по столбцам, может быть, 2 вектора __m256i одновременно, чтобы сделать целые строки кэша. Давление регистра может быть проблемой, если вы хотите расширить диапазон без AVX512. Блокировка кеша может помочь избежать потери трафика кеша (особенно пространственных предварительных выборок L2, которые вы никогда не загружаете до возникновения конфликта). - person Peter Cordes; 21.10.2019
comment
@WBuck и Гарольд: О, см. также github.com/mklarqvist/positional-popcount. Он имеет смешанную версию SSE и harley-seal AVX512 с перечисленными значительными ускорениями. (Но это позиционно в k-битных словах, а не в целых строках многих слов, если только это не работает для очень больших k). Harley-Seal: Я думаю, именно этим занимается Гарольд, внедряя полные сумматоры с разными битами позиционного значения в разных регистрах. Он уменьшает 3 входа до 2 и может применяться повторно. Так что да, идущие вниз столбцы должны работать хорошо. - person Peter Cordes; 22.10.2019
comment
github.com/WojciechMula/sse-popcount/ blob/master/ имеет горизонтальный/плоский popcnt над большим массивом, который дает один скалярный результат, но это пример Harley-Seal только с AVX2, а не AVX512 для vpternlogd. - person Peter Cordes; 22.10.2019
comment
@harold Итак, я просматривал код для последнего примера, и я немного запутался в самых внутренних циклах for. Похоже, что мы постоянно перебираем одни и те же 255 uint64 значения 255 раза. Я ожидал, что он будет перебирать начальное значение uint64 для каждой из 255 строк, а затем переходить x к следующему uint64 в первой строке. Внутренние циклы for будут смещены от новой позиции смещения ptr_data. Также обратите внимание, что я, вероятно, неправильно истолковал код, и мне просто нужно больше времени, чтобы пройти через него. - person WBuck; 22.10.2019
comment
Исправление, я хотел сказать. Похоже, что мы непрерывно перебираем одни и те же 255 значений uint64 10 раз. - person WBuck; 22.10.2019
comment
@WBuck да, я исправил это сейчас, забыл y. Кстати, странно, как это не сильно меняет производительность. Кроме того, природа данных «все единицы» не так хороша для отладки, лол, результат почти всегда один и тот же, даже если код неверен. - person harold; 22.10.2019
comment
@harold да, мне пришлось изменить тестовые данные, чтобы увидеть ошибку. Все 1 не помогало, ха-ха - person WBuck; 22.10.2019