(РЕДАКТИРОВАТЬ: Давайте назовем это «Уроки того, как измерения могут пойти не так.» Я до сих пор не выяснил, что именно вызывает эти расхождения.)
Я нашел очень быструю функцию целочисленного квадратного корня здесь Марка Крауна. По крайней мере, с GCC на моей машине, это, безусловно, самая быстрая функция с целочисленным квадратным корнем, которую я тестировал (включая функции в Hacker's Delight, this page и floor (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 .)
Короче говоря, есть ли способ написать это как inline без удара по скорости? (Я не профилировал, но sqrt - одна из тех фундаментальных операций, которые всегда должны выполняться быстро, поскольку я могу использовать ее во многих других программах, кроме той, которая мне интересна в настоящее время. Кроме того, мне просто любопытно .)
Я даже пытался использовать шаблоны для "запекания" константного значения, но мне кажется, что другие два параметра с большей вероятностью будут вызывать попадание (и макрос может этого избежать, поскольку он напрямую использует локальные переменные) ) ... ну, или что либо компилятор упорно отказывается инлайн.
ОБНОВЛЕНИЕ: пользователь user1034749 ниже получает один и тот же вывод сборки из обеих функций, когда он помещает их в отдельные файлы и компилирует их. Я попробовал его точную командную строку, и я получаю тот же результат, что и он. Для всех намерений и целей этот вопрос решен.
Однако я все еще хотел бы знать, почему мои измерения выходят по-другому. Очевидно, что мой измерительный код или оригинальный процесс сборки вызывали изменения. Я выложу код ниже. Кто-нибудь знает, в чем заключалась сделка? Может быть, мой компилятор на самом деле встроил всю функцию 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-разрядных целочисленных входных данных без потери точности, хотя (и первое преобразование со знаком также должно было бы быть заменено встроенной загрузкой для обработки значений> = 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);
}