Могу ли я изменить этот макрос на встроенную функцию без снижения производительности?

(РЕДАКТИРОВАТЬ: Давайте назовем это «Уроки того, как измерения могут пойти не так.» Я до сих пор не выяснил, что именно вызывает расхождение.)

Я нашел очень быструю функцию целочисленного квадратного корняВот Марк Краун. По крайней мере, с GCC на моей машине, это, безусловно, самая быстрая целочисленная функция квадратного корня, которую я тестировал (включая функции в Delight Hacker's,эта страницаи пол (sqrt ()) из стандартной библиотеки).

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

static uint32_t mcrowne_isqrt(uint32_t val)
{
    uint32_t temp, root = 0;

    if (val >= 0x40000000)
    {
        root = 0x8000;
        val -= 0x40000000;
    }

    #define INNER_ISQRT(s)                              \
    do                                                  \
    {                                                   \
        temp = (root << (s)) + (1 << ((s) * 2 - 2));    \
        if (val >= temp)                                \
        {                                               \
            root += 1 << ((s)-1);                       \
            val -= temp;                                \
        }                                               \
    } while(0)

    INNER_ISQRT(15);
    INNER_ISQRT(14);
    INNER_ISQRT(13);
    INNER_ISQRT(12);
    INNER_ISQRT(11);
    INNER_ISQRT(10);
    INNER_ISQRT( 9);
    INNER_ISQRT( 8);
    INNER_ISQRT( 7);
    INNER_ISQRT( 6);
    INNER_ISQRT( 5);
    INNER_ISQRT( 4);
    INNER_ISQRT( 3);
    INNER_ISQRT( 2);

    #undef INNER_ISQRT

    temp = root + root + 1;
    if (val >= temp)
        root++;
    return root;
}

Макрос INNER_ISQRT не слишком злой, поскольку он локальный и сразу не определен после того, как он больше не нужен. Тем не менее, я все же хотел бы преобразовать его в встроенную функцию в принципе. Я прочитал утверждения в нескольких местах (включая документацию GCC), что встроенные функции "так же быстры", как макросы, но у меня были проблемы с преобразованием без потери скорости.

Моя текущая итерация выглядит следующим образом (обратите внимание на атрибут always_inline, который я добавил для правильной оценки):

static inline void inner_isqrt(const uint32_t s, uint32_t& val, uint32_t& root) __attribute__((always_inline));
static inline void inner_isqrt(const uint32_t s, uint32_t& val, uint32_t& root)
{
    const uint32_t temp = (root << s) + (1 << ((s << 1) - 2));
    if(val >= temp)
    {
        root += 1 << (s - 1);
        val -= temp;
    }
}

//  Note that I just now changed the name to mcrowne_inline_isqrt, so people can compile my full test.
static uint32_t mcrowne_inline_isqrt(uint32_t val)
{
    uint32_t root = 0;

    if(val >= 0x40000000)
    {
        root = 0x8000; 
        val -= 0x40000000;
    }

    inner_isqrt(15, val, root);
    inner_isqrt(14, val, root);
    inner_isqrt(13, val, root);
    inner_isqrt(12, val, root);
    inner_isqrt(11, val, root);
    inner_isqrt(10, val, root);
    inner_isqrt(9, val, root);
    inner_isqrt(8, val, root);
    inner_isqrt(7, val, root);
    inner_isqrt(6, val, root);
    inner_isqrt(5, val, root);
    inner_isqrt(4, val, root);
    inner_isqrt(3, val, root);
    inner_isqrt(2, val, root);

    const uint32_t temp = root + root + 1;
    if (val >= temp)
        root++;
    return root;
}

Независимо от того, что я делаю, встроенная функция всегда медленнее, чем макрос. Макро версия обычно составляет около 2,92 с для (2 ^ 28 - 1) итераций со сборкой -O2, тогда как встроенная версия обычно имеет время около 3,25 с. РЕДАКТИРОВАТЬ: я сказал 2 ^ 32 - 1 итерации раньше, но я забыл, что я изменил его. Они занимают немного больше времени для полной гаммы.

Возможно, что компилятор просто глуп и отказывается встроить его (обратите внимание, снова на атрибут always_inline!), Но если это так, это в любом случае сделает версию макроса в целом более предпочтительной. (Я попытался проверить сборку, чтобы увидеть, но она была слишком сложной как часть программы. Оптимизатор, конечно, пропустил все, когда я пытался скомпилировать только функции, и у меня возникают проблемы при компиляции как библиотеки из-за неряшливости с GCC .)

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

Я даже пытался использовать шаблоны для «запекания» константного значения, но я чувствую, что другие два параметра, скорее всего, будут вызывать попадание (и макрос может этого избежать, поскольку он напрямую использует локальные переменные). .well, либо тот, либо компилятор упрямо отказывается встроить.

ОБНОВЛЕНИЕ: пользователь 1034749 ниже получает одинаковые выходные данные сборки из обеих функций, когда он помещает их в отдельные файлы и компилирует их. Я попробовал его точную командную строку, и я получаю тот же результат, что и он. По сути, этот вопрос решен.

Тем не менее, я все еще хотел бы знать, почему мои измерения выходят по-другому. Очевидно, что мой измерительный код или оригинальный процесс сборки вызывали изменения. Я выложу код ниже. Кто-нибудь знает, в чем заключалась сделка? Может быть, мой компилятор на самом деле встраивает всю функцию mcrowne_isqrt () в цикл моей функции main (), но он не встраивает в себя всю другую версию?

ОБНОВЛЕНИЕ 2 (сжато перед тестированием кода): обратите внимание, что если я поменяю местами порядок тестов и сначала сделаю встроенную версию, встроенная версия выходит быстрее, чем версия макроса на ту же величину. Это проблема кеширования, или компилятор включает один вызов, а не другой, или как?

#include <iostream>
#include <time.h>      //  Linux high-resolution timer
#include <stdint.h>

/*  Functions go here */

timespec timespecdiff(const timespec& start, const timespec& end)
{
    timespec elapsed;
    timespec endmod = end;
    if(endmod.tv_nsec < start.tv_nsec)
    {
        endmod.tv_sec -= 1;
        endmod.tv_nsec += 1000000000;
    }

    elapsed.tv_sec = endmod.tv_sec - start.tv_sec;
    elapsed.tv_nsec = endmod.tv_nsec - start.tv_nsec;
    return elapsed;
}


int main()
{
    uint64_t inputlimit = 4294967295;
    //  Test a wide range of values
    uint64_t widestep = 16;

    timespec start, end;

    //  Time macro version:
    uint32_t sum = 0;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
    for(uint64_t num = (widestep - 1); num <= inputlimit; num += widestep)
    {
        sum += mcrowne_isqrt(uint32_t(num));
    }
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
    timespec markcrowntime = timespecdiff(start, end);
    std::cout << "Done timing Mark Crowne's sqrt variant.  Sum of results = " << sum << " (to avoid over-optimization)." << std::endl;


    //  Time inline version:
    sum = 0;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
    for(uint64_t num = (widestep - 1); num <= inputlimit; num += widestep)
    {
        sum += mcrowne_inline_isqrt(uint32_t(num));
    }
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
    timespec markcrowninlinetime = timespecdiff(start, end);
    std::cout << "Done timing Mark Crowne's inline sqrt variant.  Sum of results = " << sum << " (to avoid over-optimization)." << std::endl;

    //  Results:
    std::cout << "Mark Crowne sqrt variant time:\t" << markcrowntime.tv_sec << "s, " << markcrowntime.tv_nsec << "ns" << std::endl;
    std::cout << "Mark Crowne inline sqrt variant time:\t" << markcrowninlinetime.tv_sec << "s, " << markcrowninlinetime.tv_nsec << "ns" << std::endl;
    std::cout << std::endl;
}

ОБНОВЛЕНИЕ 3: Я до сих пор не знаю, как надежно сравнить время различных функций без времени в зависимости от порядка испытаний. Буду очень признателен за любые советы!

Однако, если кто-то еще, читающий это, интересуется быстрыми реализациями sqrt, я должен отметить: Марк Кроун тестирует код быстрее, чем любая другая чистая версия C / C ++, которую я пробовал с приличным отрывом (несмотря на проблемы с надежностью при тестировании), но следующее Код SSE выглядит так, как будто он может быть немного быстрее для скалярного 32-разрядного целого sqrt. Он не может быть обобщен для полноценных 64-битных целочисленных входов без знака, не теряя при этом точности (и первое преобразование со знаком также должно быть заменено встроенным значением load для обработки значений> = 2 ^ 63):

uint32_t sse_sqrt(uint64_t num)
{
    //  Uses 64-bit input, because SSE conversion functions treat all
    //  integers as signed (so conversion from a 32-bit value >= 2^31
    //  will be interpreted as negative).  As it stands, this function
    //  will similarly fail for values >= 2^63.
    //  It can also probably be made faster, since it generates a strange/
    //  useless movsd %xmm0,%xmm0 instruction before the sqrtsd.  It clears
    //  xmm0 first too with xorpd (seems unnecessary, but I could be wrong).
    __m128d result;
    __m128d num_as_sse_double = _mm_cvtsi64_sd(result, num);
    result = _mm_sqrt_sd(num_as_sse_double, num_as_sse_double);
    return _mm_cvttsd_si32(result);
}

Ответы на вопрос(1)

Ваш ответ на вопрос