(编辑:让我们称之为“测量可能出错的经验教训。”我仍然没有弄清楚究竟是什么导致了这种差异。)
我找到了Mark Crowne的一个非常快速的整数平方根函数here。至少在我的机器上使用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 ^ 28 - 1)次迭代,使用-O2构建时,宏版本通常在2.92s左右,而内联版本通常在3.25s左右。编辑:我之前说过2 ^ 32 - 1次迭代,但我忘记了我已经改变了它。对于全部色域,它们需要更长的时间。
编译器可能只是愚蠢并且拒绝内联它(再次注意always_inline属性!),但如果是这样,那么无论如何都会使宏版本更受欢迎。 (我尝试检查程序集,但它作为程序的一部分太复杂。当我尝试编译函数时,优化器省略了所有内容,并且由于GCC的无效性,我将问题编译为库。)
简而言之,有没有办法在没有速度命中的情况下将其写为内联? (我没有说过,但是sqrt是应该总是快速完成的基本操作之一,因为我可能在许多其他程序中使用它而不仅仅是我目前感兴趣的程序。此外,我只是好奇。)
我甚至尝试使用模板“烘焙”常量值,但我感觉其他两个参数更可能导致命中(并且宏可以避免这种情况,因为它直接使用局部变量)......好吧,无论是那个还是编译器都顽固地拒绝内联。
更新:下面的user1034749在将它们放入单独的文件并编译它们时从两个函数获得相同的程序集输出。我尝试了他的确切命令行,我得到了与他相同的结果。出于所有意图和目的,这个问题得到了解决。
然而,我仍然想知道为什么我的测量结果不同。显然,我的测量代码或原始构建过程导致事物变得不同。我将在下面发布代码。有谁知道这笔交易是什么?也许我的编译器实际上是在我的main()函数的循环中内联整个mcrowne_isqrt()函数,但是它没有内联其他版本的全部内容?
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'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实现感兴趣,我应该提一下:Mark Crowne的代码测试速度比任何其他纯C / C ++版本都要快得多(尽管测试存在可靠性问题),但是对于标量32位整数sqrt,以下SSE代码看起来可能还要快一点。它不能推广到完整的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);
}
答案 0 :(得分:7)
我用gcc 4.5.3尝试了你的代码。 我修改了第二个版本的代码以匹配第一个版本, 例如:
(1 << ((s) * 2 - 2)
VS
(1 << ((s << 1) - 1)
是的,s * 2 == s&lt;&lt; 1,但“-2”和“-1”?
另外我修改了你的类型用“unsigned long”替换uint32_t,因为在我的64位机器上“long”不是32位数。
然后我跑:
g++ -ggdb -O2 -march=native -c -pipe inline.cpp
g++ -ggdb -O2 -march=native -c -pipe macros.cpp
objdump -d inline.o > inline.s
objdump -d macros.o > macros.s
我可以使用“-S”代替“-c”来汇编,但我希望看到汇编程序没有其他信息。
你知道吗? 汇编程序完全相同, 在第一个和第二个版本中。 所以我认为你的时间测量是错误的。