(EDIT: дайте название этому, "Уроки, как измерения могут пойти не так". Я все еще не понял, что именно вызывает расхождение.)
Я нашел очень быструю функцию квадратного корня целого числа здесь от Марка Короны. По крайней мере, с GCC на моей машине, это, несомненно, самая быстрая функция квадратного корня, которую я тестировал (включая функции в Hacker Delight, эту страницу, и 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 не слишком злой, так как он локальный и сразу undefined после того, как он больше не нужен. Тем не менее, я все равно хотел бы преобразовать его в встроенную функцию в принципе. Я читал утверждения в нескольких местах (включая документацию 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.92s для (2 ^ 28 - 1) итераций с -O2 build, тогда как встроенная версия обычно занимает около 3.25s. EDIT: Я сказал 2 ^ 32 - 1 итераций раньше, но я забыл, что я изменил его. Они занимают довольно много времени для полной гаммы.
Возможно, что компилятор просто глуп и отказывается встраивать его (обратите внимание снова на атрибут always_inline!), но если это так, это сделает версию макроса вообще предпочтительной в любом случае. (Я попытался проверить сборку, чтобы увидеть ее, но она была слишком сложной, как часть программы. Оптимизатор опустил все, когда я попытался скомпилировать только функции, конечно, и у меня возникли проблемы с компиляцией его как библиотеки из-за noobishness с GCC.)
Короче говоря, есть ли способ написать это как встроенную строку без быстрого нажатия? (Я не профилировал, но sqrt - одна из тех фундаментальных операций, которые всегда должны выполняться быстро, поскольку я могу использовать ее во многих других программах, кроме тех, которые мне сейчас интересны. Кроме того, мне просто интересно.)
Я даже пытался использовать шаблоны, чтобы "испечь" постоянное значение, но я чувствую, что другие два параметра, скорее всего, будут причиной хита (и макрос может избежать этого, поскольку он напрямую использует локальные переменные)... ну, либо это, либо компилятор упрямо отказывается от встроенного.
UPDATE: user1034749 ниже получает один и тот же сборный вывод из обеих функций, когда он помещает их в отдельные файлы и компилирует их. Я пробовал его точную командную строку, и я получаю тот же результат, что и он. По всем вопросам и задачам этот вопрос решается.
Однако мне все же хотелось бы знать, почему мои измерения выходят по-другому. Очевидно, что мой код измерения или исходный процесс сборки вызывали разные вещи. Я отправлю код ниже. Кто-нибудь знает, что это за сделка? Может быть, мой компилятор на самом деле встраивает всю функцию mcrowne_isqrt() в цикле моей функции main(), но не вписывает полную версию другой версии?
UPDATE 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 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 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, я должен упомянуть: код Mark Crowne тестируется быстрее, чем любая другая чистая версия 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);
}