Как стать автором
Обновить

Комментарии 32

а тесты на производительность, интересно же

А если использовать AVX или AVX-512?

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

AVX & AVX-512 сделают код короче и быстрей, но не в разы.

Есть обновление, тесты AndreyDmitriev показали, что лучший на данный момент код на AVX-512 в 10 раз быстрей. Возможно мой код можно ускорить в два и даже три раза, но все равно AVX-512 будет в три пять раз быстрей.

Мне кажется, что использование MMX одновременно с XMM слегка снижает производительность, так что рекомендую рассмотреть регистры XMM8-XMM15, тем более код все равно 64-битный.

p.s. ссылка в конце битая и ведет в "https://sha_256/"

У меня нет оснований для такого утверждения, но я предполагаю что регистры MMX & XMM с аппаратной точки зрения это одни и те же регистры, где в качестве MMX регистра выступает нижняя часть MMX регистра, таким образом мой код просто "отжимает" себе больше регистров из общей "кучи". К примеру в Skylake 128 векторных регистров, разделяемых между 6-8 ядрами.

Согласен что решение через старшие регистры SIMD "безопасней".

MMX одновременно с XMM слегка снижает производительность

Тут, видимо, имеются ввиду пенальти, связанные с переходом процессора из одного состояния (AVX) в другое (SSE) и необходимостью сохранить верхние 128 бит каждого YMM регистра. Но это в общем лечится использованием vzeroupper, которая обнуляет эти биты, но всё же учебник не рекомендует смешивать такой код.

Цитата из учебника (на английском)

The last issue that programmers need to be aware of involves the intermixing of x86-AVX and x86-SSE code. Programs are allowed to intermix x86-AVX and x86-SSE instructions, but any intermixing should be kept to a minimum in order avoid internal processor state transition penalties that can affect performance.
These penalties can occur if the processor is required to preserve the upper 128 bits of each YMM register during a transition from executing x86-AVX to executing x86-SSE instructions. State transition penalties can be completely avoided by using the vzeroupper (Zero Upper Bits of YMM Registers) instruction, which zeroes the upper 128 bits of all YMM registers. This instruction should be used prior to any transition from 256-bit x86-AVX code (i.e., any x86-AVX code that uses a YMM register) to x86-SSE code.
One common use of the vzeroupper instruction is by a public function that uses 256-bit x86-AVX instructions. These types of functions should include a vzeroupper instruction prior to the execution of any ret instruction since this prevents processor state transition penalties from occurring in any high-level language code that uses x86-SSE instructions. The vzeroupper instruction should also be employed before calling any library functions that might contain x86-SSE code. Later in this book, you’ll see several source code examples that demonstrate proper use of the vzeroupper instruction. Functions can also use the vzeroall (Zero All YMM Registers) instruction instead of vzeroupper to avoid potential x86-AVX/x86-SSE state transition penalties.

Если не выходить за пределы SSE то пенальти не будет назначено.

Не разделённых между ядрами, а на одно ядро.

Попробую, проверю.

Уважаю задачи по оптимизации, да тест на производительность не помешал бы. В тексте ссылку в на github поправьте.

Поправил

Там точно Hesh а не Hash вычисляется?

Что-то мне подсказывает, что для "r" регистров надо movq использовать вместо movd.

Вот здесь (и ещё в куче мест):

Я просто хотел себе динамическую библиотечку забацать, но налетел на исключение, и вот проходя отладчиком, заметил, что movd местами превратились в movq (там где eax - осталось movd, естественно) :

Ну то есть оно у вас и так работает, просто masm достаточно умный, а если сразу movq написать, то будет аккуратнее, как мне кажется.

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

Так и живем.

Забавно, но вот только что я узнал ответ на этот вопрос.

Изначально инструкция movd появилась вместе с ММХ до х64 и могла пересылать только 32-битные данные из GPR в MMX, а инструкция movq пересылала данные между регистрами MMX, потом регистры расширили, а мнемонику менять не стали, и по прежнему movd пересылает данные между разными регистрами, а movq между регистрами ММХ.

Где-там унутре, похоже сидит бага.

Хеш от строки "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123" должен быть "8FB605EAB2EFAE3D1FCC881FA5C5DD6219A17CA3663E46642FF566847C24C272", а алгоритм выдаёт "CE9C5B8AEF93B3DBA226776FD28705501FEF649A50C3257D65DFE2DC42997E3A" (если я всё правильно скомпилировал). Однако если я уберу последний символ: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012", то становится правильно: "D74BA075E4259C6C807C4101E66D281096CF9FF14BA01260DEE741B1BDAEF326". Вообще для всех строк короче 55 символов вроде ОК, а вот начиная с 56 байт и длиннее — неверно. Я глубоко не копал, это навскидку так. Константы вроде верные, я проверил.

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

Что еще раз напоминает об опасности работы с указателями.

Большое Вам спасибо за внимание к моему коду и его проверку.

Надеюсь он Вам пригодится.

Спасибо за правку. Да, теперь этот тест проходит, но граница сдвинулась на 64-й байт. Тестовая строка длиной 64 байта - слово test 16 раз — "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest". Должно быть "3e2b0a3dc3503d99e14cf834a3be419c4729fe32ee5fd037407f81f4d73aa619", а у Вас (точнее у меня) "4fbce22b8a9bf8137c3d2d0ad0a3cb2ea63d37be47cfdc5ea99f0a958337aca7". Я для проверки вот этим сервисом пользуюсь.

Посмотрите пожалйста, если найдётся время. Практического интереса у меня в общем нет, просто нравятся такие мини-проектики, хочу на мегабайтной строке побенчмаркать и сравнить с OpenSSL и LabVIEW.

Исправил.

В процедуре загрузки я не учел, что "заглушку" тоже нужно перевернуть.

Попробуйте сейчас.

Отлично, на рандомной мегабайтной строке тест проходит!

На досуге время замеряю.

По бенчмаркам вот что получается, если взять 16 МБ строку:

На стареньком рабочем лаптопе вот так:

LabVIEW Вы уверенно обогнали, но до OpenSSL не дотянулись, примерно втрое

На Xeon W-2245, тут частота повыше и результаты получше:

А если запустить на камушке, который, насколько я понимаю, нативно поддерживает SHA256, то вот:

Тут уже ровно десятикратная разница.

Надо попробовать убрать MMX и перенести все в SIMD

На wiki написано, что часть OpenSSL написан на ассемблере, так что возможно тут соревнуются два асм кода и похоже их лучше.

А, и вдогонку, одна из самых быстрых реализаций выглядит как-то вот так:

Если процессор поддерживает SHA256RNDS2, SHA256MSG1 и SHA256MSG2
/* sha256-x86.c - Intel SHA extensions using C intrinsics  */
/*   Written and place in public domain by Jeffrey Walton  */
/*   Based on code from Intel, and by Sean Gulley for      */
/*   the miTLS project.                                    */

/* gcc -DTEST_MAIN -msse4.1 -msha sha256-x86.c -o sha256.exe   */

/* Include the GCC super header */
#if defined(__GNUC__)
# include <stdint.h>
# include <x86intrin.h>
#endif

/* Microsoft supports Intel SHA ACLE extensions as of Visual Studio 2015 */
#if defined(_MSC_VER)
# include <immintrin.h>
# define WIN32_LEAN_AND_MEAN
# include <Windows.h>
typedef UINT32 uint32_t;
typedef UINT8 uint8_t;
#endif

/* Process multiple blocks. The caller is responsible for setting the initial */
/*  state, and the caller is responsible for padding the final block.        */
void sha256_process_x86(uint32_t state[8], const uint8_t data[], uint32_t length)
{
    __m128i STATE0, STATE1;
    __m128i MSG, TMP;
    __m128i MSG0, MSG1, MSG2, MSG3;
    __m128i ABEF_SAVE, CDGH_SAVE;
    const __m128i MASK = _mm_set_epi64x(0x0c0d0e0f08090a0bULL, 0x0405060700010203ULL);

    /* Load initial values */
    TMP = _mm_loadu_si128((const __m128i*) &state[0]);
    STATE1 = _mm_loadu_si128((const __m128i*) &state[4]);


    TMP = _mm_shuffle_epi32(TMP, 0xB1);          /* CDAB */
    STATE1 = _mm_shuffle_epi32(STATE1, 0x1B);    /* EFGH */
    STATE0 = _mm_alignr_epi8(TMP, STATE1, 8);    /* ABEF */
    STATE1 = _mm_blend_epi16(STATE1, TMP, 0xF0); /* CDGH */

    while (length >= 64)
    {
        /* Save current state */
        ABEF_SAVE = STATE0;
        CDGH_SAVE = STATE1;

        /* Rounds 0-3 */
        MSG = _mm_loadu_si128((const __m128i*) (data+0));
        MSG0 = _mm_shuffle_epi8(MSG, MASK);
        MSG = _mm_add_epi32(MSG0, _mm_set_epi64x(0xE9B5DBA5B5C0FBCFULL, 0x71374491428A2F98ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);

        /* Rounds 4-7 */
        MSG1 = _mm_loadu_si128((const __m128i*) (data+16));
        MSG1 = _mm_shuffle_epi8(MSG1, MASK);
        MSG = _mm_add_epi32(MSG1, _mm_set_epi64x(0xAB1C5ED5923F82A4ULL, 0x59F111F13956C25BULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG0 = _mm_sha256msg1_epu32(MSG0, MSG1);

        /* Rounds 8-11 */
        MSG2 = _mm_loadu_si128((const __m128i*) (data+32));
        MSG2 = _mm_shuffle_epi8(MSG2, MASK);
        MSG = _mm_add_epi32(MSG2, _mm_set_epi64x(0x550C7DC3243185BEULL, 0x12835B01D807AA98ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG1 = _mm_sha256msg1_epu32(MSG1, MSG2);

        /* Rounds 12-15 */
        MSG3 = _mm_loadu_si128((const __m128i*) (data+48));
        MSG3 = _mm_shuffle_epi8(MSG3, MASK);
        MSG = _mm_add_epi32(MSG3, _mm_set_epi64x(0xC19BF1749BDC06A7ULL, 0x80DEB1FE72BE5D74ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG3, MSG2, 4);
        MSG0 = _mm_add_epi32(MSG0, TMP);
        MSG0 = _mm_sha256msg2_epu32(MSG0, MSG3);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG2 = _mm_sha256msg1_epu32(MSG2, MSG3);

        /* Rounds 16-19 */
        MSG = _mm_add_epi32(MSG0, _mm_set_epi64x(0x240CA1CC0FC19DC6ULL, 0xEFBE4786E49B69C1ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG0, MSG3, 4);
        MSG1 = _mm_add_epi32(MSG1, TMP);
        MSG1 = _mm_sha256msg2_epu32(MSG1, MSG0);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG3 = _mm_sha256msg1_epu32(MSG3, MSG0);

        /* Rounds 20-23 */
        MSG = _mm_add_epi32(MSG1, _mm_set_epi64x(0x76F988DA5CB0A9DCULL, 0x4A7484AA2DE92C6FULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG1, MSG0, 4);
        MSG2 = _mm_add_epi32(MSG2, TMP);
        MSG2 = _mm_sha256msg2_epu32(MSG2, MSG1);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG0 = _mm_sha256msg1_epu32(MSG0, MSG1);

        /* Rounds 24-27 */
        MSG = _mm_add_epi32(MSG2, _mm_set_epi64x(0xBF597FC7B00327C8ULL, 0xA831C66D983E5152ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG2, MSG1, 4);
        MSG3 = _mm_add_epi32(MSG3, TMP);
        MSG3 = _mm_sha256msg2_epu32(MSG3, MSG2);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG1 = _mm_sha256msg1_epu32(MSG1, MSG2);

        /* Rounds 28-31 */
        MSG = _mm_add_epi32(MSG3, _mm_set_epi64x(0x1429296706CA6351ULL,  0xD5A79147C6E00BF3ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG3, MSG2, 4);
        MSG0 = _mm_add_epi32(MSG0, TMP);
        MSG0 = _mm_sha256msg2_epu32(MSG0, MSG3);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG2 = _mm_sha256msg1_epu32(MSG2, MSG3);

        /* Rounds 32-35 */
        MSG = _mm_add_epi32(MSG0, _mm_set_epi64x(0x53380D134D2C6DFCULL, 0x2E1B213827B70A85ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG0, MSG3, 4);
        MSG1 = _mm_add_epi32(MSG1, TMP);
        MSG1 = _mm_sha256msg2_epu32(MSG1, MSG0);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG3 = _mm_sha256msg1_epu32(MSG3, MSG0);

        /* Rounds 36-39 */
        MSG = _mm_add_epi32(MSG1, _mm_set_epi64x(0x92722C8581C2C92EULL, 0x766A0ABB650A7354ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG1, MSG0, 4);
        MSG2 = _mm_add_epi32(MSG2, TMP);
        MSG2 = _mm_sha256msg2_epu32(MSG2, MSG1);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG0 = _mm_sha256msg1_epu32(MSG0, MSG1);

        /* Rounds 40-43 */
        MSG = _mm_add_epi32(MSG2, _mm_set_epi64x(0xC76C51A3C24B8B70ULL, 0xA81A664BA2BFE8A1ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG2, MSG1, 4);
        MSG3 = _mm_add_epi32(MSG3, TMP);
        MSG3 = _mm_sha256msg2_epu32(MSG3, MSG2);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG1 = _mm_sha256msg1_epu32(MSG1, MSG2);

        /* Rounds 44-47 */
        MSG = _mm_add_epi32(MSG3, _mm_set_epi64x(0x106AA070F40E3585ULL, 0xD6990624D192E819ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG3, MSG2, 4);
        MSG0 = _mm_add_epi32(MSG0, TMP);
        MSG0 = _mm_sha256msg2_epu32(MSG0, MSG3);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG2 = _mm_sha256msg1_epu32(MSG2, MSG3);

        /* Rounds 48-51 */
        MSG = _mm_add_epi32(MSG0, _mm_set_epi64x(0x34B0BCB52748774CULL, 0x1E376C0819A4C116ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG0, MSG3, 4);
        MSG1 = _mm_add_epi32(MSG1, TMP);
        MSG1 = _mm_sha256msg2_epu32(MSG1, MSG0);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);
        MSG3 = _mm_sha256msg1_epu32(MSG3, MSG0);

        /* Rounds 52-55 */
        MSG = _mm_add_epi32(MSG1, _mm_set_epi64x(0x682E6FF35B9CCA4FULL, 0x4ED8AA4A391C0CB3ULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG1, MSG0, 4);
        MSG2 = _mm_add_epi32(MSG2, TMP);
        MSG2 = _mm_sha256msg2_epu32(MSG2, MSG1);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);

        /* Rounds 56-59 */
        MSG = _mm_add_epi32(MSG2, _mm_set_epi64x(0x8CC7020884C87814ULL, 0x78A5636F748F82EEULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        TMP = _mm_alignr_epi8(MSG2, MSG1, 4);
        MSG3 = _mm_add_epi32(MSG3, TMP);
        MSG3 = _mm_sha256msg2_epu32(MSG3, MSG2);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);

        /* Rounds 60-63 */
        MSG = _mm_add_epi32(MSG3, _mm_set_epi64x(0xC67178F2BEF9A3F7ULL, 0xA4506CEB90BEFFFAULL));
        STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG);
        MSG = _mm_shuffle_epi32(MSG, 0x0E);
        STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG);

        /* Combine state  */
        STATE0 = _mm_add_epi32(STATE0, ABEF_SAVE);
        STATE1 = _mm_add_epi32(STATE1, CDGH_SAVE);

        data += 64;
        length -= 64;
    }

    TMP = _mm_shuffle_epi32(STATE0, 0x1B);       /* FEBA */
    STATE1 = _mm_shuffle_epi32(STATE1, 0xB1);    /* DCHG */
    STATE0 = _mm_blend_epi16(TMP, STATE1, 0xF0); /* DCBA */
    STATE1 = _mm_alignr_epi8(STATE1, TMP, 8);    /* ABEF */

    /* Save state */
    _mm_storeu_si128((__m128i*) &state[0], STATE0);
    _mm_storeu_si128((__m128i*) &state[4], STATE1);
}

#if defined(TEST_MAIN)

#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
    /* empty message with padding */
    uint8_t message[64];
    memset(message, 0x00, sizeof(message));
    message[0] = 0x80;

    /* initial state */
    uint32_t state[8] = {
        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
        0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
    };

    sha256_process_x86(state, message, sizeof(message));

    const uint8_t b1 = (uint8_t)(state[0] >> 24);
    const uint8_t b2 = (uint8_t)(state[0] >> 16);
    const uint8_t b3 = (uint8_t)(state[0] >>  8);
    const uint8_t b4 = (uint8_t)(state[0] >>  0);
    const uint8_t b5 = (uint8_t)(state[1] >> 24);
    const uint8_t b6 = (uint8_t)(state[1] >> 16);
    const uint8_t b7 = (uint8_t)(state[1] >>  8);
    const uint8_t b8 = (uint8_t)(state[1] >>  0);

    /* e3b0c44298fc1c14... */
    printf("SHA256 hash of empty message: ");
    printf("%02X%02X%02X%02X%02X%02X%02X%02X...\n",
        b1, b2, b3, b4, b5, b6, b7, b8);

    int success = ((b1 == 0xE3) && (b2 == 0xB0) && (b3 == 0xC4) && (b4 == 0x42) &&
                    (b5 == 0x98) && (b6 == 0xFC) && (b7 == 0x1C) && (b8 == 0x14));

    if (success)
        printf("Success!\n");
    else
        printf("Failure!\n");

    return (success != 0 ? 0 : 1);
}

#endif

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

А собственно OpenSSL, что в тесте выше использовалась, как-то так устроена:

OpenSSL
#include <openssl/evp.h>

// OpenSSL
SHA256DLL_API int fnSHA256OPENSSL(uint8_t* src, size_t n, uint8_t* dst)
{
    EVP_MD_CTX* mdctx;
    const EVP_MD* md;
    unsigned char md_value[EVP_MAX_MD_SIZE];
    unsigned int md_len;

    md = EVP_get_digestbyname("SHA256"); if (!md) return -1;
    mdctx = EVP_MD_CTX_new(); if (!mdctx) return -2;
    if (!EVP_DigestInit_ex2(mdctx, md, NULL)) { EVP_MD_CTX_free(mdctx); return -3; }
    if (!EVP_DigestUpdate(mdctx, src, n)) { EVP_MD_CTX_free(mdctx);  return -4; }
    if (!EVP_DigestFinal_ex(mdctx, md_value, &md_len)) { EVP_MD_CTX_free(mdctx); return -5; }
    EVP_MD_CTX_free(mdctx);

    memcpy_s(dst, 32, md_value, md_len);

    return md_len;
}

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

Ну и если задуматься то весь этот код по сути ассемблерный, но обернутый в С-подобный синтаксис, что позволяет значительно легче интегрировать его в целевой проект.

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

весь этот код по сути ассемблерный, но обернутый в С-подобный синтаксис, 

В обиходе это Интринсиками называется. Intel Intrinsics Guide. Штука удобная, потому что во-первых можно комбинировать команды:

й MSG = _mm_add_epi32(MSG1, _mm_set_epi64x(0xAB1..., 0x59F...));

А во-вторых, заводить переменные не заботясь о регистрах, которых вечно не хватает, компилятор сам раскидает как надо:

    __m128i STATE0, STATE1;
    __m128i MSG, TMP;
    __m128i MSG0, MSG1, MSG2, MSG3;
    __m128i ABEF_SAVE, CDGH_SAVE;
    const __m128i MASK = _mm_set_epi64x(

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

хитрый способ заставить проц грузить данные в кеш заблаговременно

Ну да, PREFETCH, PREFETCHNTA, PREFETCHT0 — PREFETCHT2 называется. Но на современных процессорах заметного влияния почти не оказывает, а позапрошлым летом упражнялся, разница гомеопатическая. Говорят надо примерно за сотню-другую тактов до обращения к памяти префетч дёрнуть, но я не увидел разницы, как бы не старался.

Можно ещё профилировщиком Intel VTune пробежать, по крайней мере он "горячие точки" покажет. У Вас бóльшая часть времени вот где-то здесь проходит:

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

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

Кстати, для "тонкой" профилировки кода с подсчётом тактов можно воспользоваться комбинацией cpuid/rdtsc.

Я обычно заворачиваю код в DLL и на ассемблере делаю как-то так:

		align 16
EXPORT fnRtdsc_empty
fnRtdsc_empty PROC
	mov r10, rcx ; x64 calling convention
	push rsi
	push rdi
	push rbx
	push rcx
	push rdx

	cpuid ; force all previous instructions to complete
		  ; this will reset rax...rdx registerss!
	rdtsc ; read time stamp counter
	mov edi, eax ; save start EAX for later
	mov esi, edx ; save start EDX for later
L0:
	; CODE to be tested
    ; вставлять сюда
	dec	r10
	jnz	L0

	cpuid ; wait for FDIV to complete before RDTSC
	rdtsc ; read time stamp counter
	sub eax, edi ; subtract the most recent CPU ticks from the original CPU ticks
	sbb edx, esi ; now, subtract with borrow
    shl rax, 32
	shrd rax, rdx, 32
        
	pop rdx
	pop rcx
	pop rbx
	pop rdi
	pop rsi

    RET ;returned through RAX
ENDP fnRtdsc_empty

Эта функция вызывается

int diff = fnRtdsc_empty(4096);

Параметр - это сколько раз надо внутренний цикл крутить. Дальше я кручу эту функцию этак сотню тысяч раз и выбираю минимальное время.

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

L1:
	; CODE
	add rax, rax
	add rax, rax
	add rax, rax
	add rax, rax  ; тут латентность 4 такта на 4 сложения
	dec	r10
	jnz	L1

Или этак:

L2:
	; CODE
	add rax, rax
	add rbx, rbx
	add rcx, rcx
	add rdx, rdx ; а тут всё параллельно за один такт		
	dec	r10
	jnz	L2

То я сразу увижу, что первый заметно медленнее.

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

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

cpuid это маленький "трюк" для "остановки" конвейеров, так то можно и rdtsc обойтись, просто команды могут параллелиться, так что их иногда парой применяют.

Я тут чутка заморочился, взял просто реализацию на чистом Си, и несколько разных компиляторов, все последние версии и старенькая студия 2010 до кучи, она просто под рукой оказалась. В общем реализация на ассемблере проигрывает, даже "слабеньким" компияляторам, а лучше всех себя интеловский показал (что в общем ожидаемо):

Вообще в современных реалиях переписывание чего либо на ассемблере редко даёт значительный выигрыш (если только AVX2/AVX512 активно не юзать), современные компиляторы очень неплохи в смысле качества генерации машинного кода, GCC оказался неожиданно хорош, почти вровень с интеловвским. Особых телодвижений по оптимизации я не принимал, всё по дефолту, дебаг выключен, О3 включена. Интеловский можно вероятно ещё подкрутить, если проверить векторизацию да разворачивание циклов, но при наличии OpenSSL смысла особо не имеет.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации