我们在我们的项目中进行了大量的浮点到整数的转换。基本上,像这样
for(int i = 0; i < HUGE_NUMBER; i++)
int_array[i] = float_array[i];
执行转换的默认 C 函数结果非常耗时。
是否有任何解决方法(可能是手动调整功能)可以稍微加快这个过程?我们不太关心精度。
我们在我们的项目中进行了大量的浮点到整数的转换。基本上,像这样
for(int i = 0; i < HUGE_NUMBER; i++)
int_array[i] = float_array[i];
执行转换的默认 C 函数结果非常耗时。
是否有任何解决方法(可能是手动调整功能)可以稍微加快这个过程?我们不太关心精度。
这里的大多数其他答案只是试图消除循环开销。
只有deft_code 的答案才能触及可能真正问题的核心——在 x86 处理器上将浮点数转换为整数是非常昂贵的。deft_code 的解决方案是正确的,尽管他没有给出引用或解释。
这是技巧的来源,有一些解释以及特定于您是要向上、向下还是向零取整的版本:了解您的 FPU
很抱歉提供一个链接,但实际上这里写的任何内容,除了复制那篇优秀的文章,都不会让事情变得清晰。
inline int float2int( double d )
{
union Cast
{
double d;
long l;
};
volatile Cast c;
c.d = d + 6755399441055744.0;
return c.l;
}
// this is the same thing but it's
// not always optimizer safe
inline int float2int( double d )
{
d += 6755399441055744.0;
return reinterpret_cast<int&>(d);
}
for(int i = 0; i < HUGE_NUMBER; i++)
int_array[i] = float2int(float_array[i]);
双参数没有错!有办法直接用浮点数来做这个技巧,但是试图覆盖所有的角落情况会变得很难看。如果您想截断而不是使用 6755399441055743.5(少 0.5),此函数将以当前形式将浮点数舍入到最接近的整数。
我对进行浮点到整数转换的不同方式进行了一些测试。简短的回答是假设您的客户拥有支持 SSE2 的 CPU 并设置 /arch:SSE2 编译器标志。这将允许编译器使用比幻数技术快两倍的 SSE标量指令。
否则,如果您有很长的浮动字符串要研磨,请使用 SSE2 打包操作。
SSE3 指令集中有一条 FISTTP 指令可以执行您想要的操作,但至于它是否可以被利用并产生比 libc 更快的结果,我不知道。
时间是否足以超过启动几个线程的成本?
假设您的机器上有一个多核处理器或多个可以利用的处理器,那么跨多个线程并行化将是一项微不足道的任务。
关键是要避免 _ftol() 函数,它不必要地慢。对于这样的长数据列表,最好的选择是使用 SSE2 指令 cvtps2dq 将两个压缩浮点数转换为两个压缩 int64。这样做两次(在两个 SSE 寄存器中获得四个 int64),您可以将它们混洗在一起以获得四个 int32(丢失每个转换结果的前 32 位)。您不需要组装即可执行此操作;MSVC 将编译器内在函数暴露给相关指令—— _mm_cvtpd_epi32()如果我的记忆正确的话。
如果你这样做,你的 float 和 int 数组必须 16 字节对齐,这样 SSE2 加载/存储内在函数才能以最大效率工作,这一点非常重要。另外,我建议您稍微使用软件管道,并在每个循环中一次处理16 个浮点数,例如(假设这里的“函数”实际上是对编译器内在函数的调用):
for(int i = 0; i < HUGE_NUMBER; i+=16)
{
//int_array[i] = float_array[i];
__m128 a = sse_load4(float_array+i+0);
__m128 b = sse_load4(float_array+i+4);
__m128 c = sse_load4(float_array+i+8);
__m128 d = sse_load4(float_array+i+12);
a = sse_convert4(a);
b = sse_convert4(b);
c = sse_convert4(c);
d = sse_convert4(d);
sse_write4(int_array+i+0, a);
sse_write4(int_array+i+4, b);
sse_write4(int_array+i+8, c);
sse_write4(int_array+i+12, d);
}
这样做的原因是 SSE 指令有很长的延迟,因此如果您立即将加载到 xmm0 并在 xmm0 上执行相关操作,那么您将遇到停顿。一次“运行”多个寄存器可以稍微隐藏延迟。(理论上,一个神奇的无所不知的编译器可以解决这个问题,但实际上它不会。)
如果此 SSE juju 失败,您可以向 MSVC 提供 /QIfist 选项,这将导致它发出单个操作码拳头而不是调用 _ftol;这意味着它将简单地使用恰好在 CPU 中设置的任何舍入模式,而不确保它是 ANSI C 的特定截断操作。微软文档说 /QIfist 已被弃用,因为他们的浮点代码现在很快,但反汇编程序会告诉你这是不合理的乐观。甚至 /fp:fast 也只会导致调用 _ftol_sse2,尽管它比令人震惊的 _ftol 更快,但它仍然是一个函数调用,然后是一个潜在的 SSE 操作,因此不必要地慢。
顺便说一下,我假设您使用的是 x86 架构——如果您使用的是 PPC,则有等效的 VMX 操作,或者您可以使用上面提到的幻数乘法技巧,然后使用 vsel(以屏蔽掉非尾数位)和对齐的存储。
您可以使用一些神奇的汇编代码将所有整数加载到处理器的 SSE 模块中,然后执行等效代码将值设置为整数,然后将它们作为浮点数读取。我不确定这会更快。我不是 SSE 专家,所以我不知道该怎么做。也许其他人可以插话。
在 Visual C++ 2008 中,编译器会自行生成 SSE2 调用,如果您使用最大化优化选项进行发布构建,并查看反汇编(尽管必须满足某些条件,请使用您的代码)。
请参阅这篇英特尔文章以加快整数转换:
http://software.intel.com/en-us/articles/latency-of-floating-point-to-integer-conversions/
根据 Microsoft 的说法,/QIfist 编译器选项在 VS 2005 中已弃用,因为整数转换已加快。他们忽略了它是如何加速的,但查看拆卸列表可能会提供线索。
http://msdn.microsoft.com/en-us/library/z8dh4h17(vs.80).aspx
大多数 c 编译器都会为每个浮点到 int 的转换生成对 _ftol 或其他东西的调用。减少浮点一致性开关(如 fp:fast)可能会有所帮助 - 如果您理解并接受此开关的其他效果。除此之外,如果您没问题并了解不同的舍入行为,请将其放入紧密组装或 sse 内在循环中。对于像您的示例这样的大型循环,您应该编写一个函数,该函数设置一次浮点控制字,然后仅使用 fistp 指令进行批量舍入,然后重置控制字 - 如果您可以使用仅 x86 的代码路径,但至少你不会改变四舍五入。阅读 fld 和 fistp fpu 指令以及 fpu 控制字。
你用的是什么编译器?在微软较新的 C/C++ 编译器中,在 C/C++ -> Code Generation -> Floating point model 下有一个选项,有选项:fast、precision、strict。我认为精确是默认设置,并且在某种程度上通过模拟 FP 操作来工作。如果您使用的是 MS 编译器,该选项是如何设置的?将其设置为“快速”是否有帮助?无论如何,拆卸是什么样的?
正如上面三十七所说,CPUfloat<->int
基本上可以在一条指令中进行转换,而且它不会比这更快(缺少 SIMD 操作)。
另请注意,现代 CPU 对单(32 位)和双(64 位)浮点数使用相同的 FP 单元,因此除非您试图节省存储大量浮点数的内存,否则真的没有理由偏爱float
双精度数。
在英特尔上,您最好的选择是内联 SSE2 调用。
我对你的结果感到惊讶。你用的是什么编译器?您是否一直在优化编译?您是否使用valgrind和 Kcachegrind 确认这是瓶颈所在?你用的是什么处理器?汇编代码是什么样的?
转换本身应编译为一条指令。一个好的优化编译器应该展开循环,以便每个测试和分支完成多个转换。如果这没有发生,您可以手动展开循环:
for(int i = 0; i < HUGE_NUMBER-3; i += 4) {
int_array[i] = float_array[i];
int_array[i+1] = float_array[i+1];
int_array[i+2] = float_array[i+2];
int_array[i+3] = float_array[i+3];
}
for(; i < HUGE_NUMBER; i++)
int_array[i] = float_array[i];
如果你的编译器真的很可怜,你可能需要用常见的子表达式来帮助它,例如,
int *ip = int_array+i;
float *fp = float_array+i;
ip[0] = fp[0];
ip[1] = fp[1];
ip[2] = fp[2];
ip[3] = fp[3];
请报告更多信息!
如果您不太关心舍入语义,则可以使用该lrint()
函数。这允许更自由的舍入,并且可以更快。
从技术上讲,它是一个 C99 函数,但您的编译器可能会在 C++ 中公开它。一个好的编译器也会将它内联到一条指令中(现代 G++ 会)。
四舍五入只有极好的技巧,只有使用 6755399441055743.5(0.5 少)进行四舍五入是行不通的。
6755399441055744 = 2^52 + 2^51 在尾数末尾溢出小数,在 fpu 寄存器的位 51 - 0 中留下您想要的整数。
在 IEEE 754
6755399441055744.0 =
符号 指数 尾数
0 10000110011 10000000000000000000000000000000000000000000000000000
然而,6755399441055743.5 也将编译为 0100001100111000000000000000000000000000000000000000000000000000
0.5 溢出结束(四舍五入),这就是为什么它首先起作用的原因。
要进行截断,您必须将 0.5 添加到您的 double 中,然后执行此操作,保护数字应负责以这种方式舍入到正确的结果。还要注意 64 位 gcc linux,其中 long 相当烦人地意味着 64 位整数。
如果您有非常大的数组(大于几 MB - CPU 缓存的大小),请为您的代码计时并查看吞吐量是多少。您可能正在使内存总线饱和,而不是 FP 单元。查找您的 CPU 的最大理论带宽,看看您离它有多近。
如果你受到内存总线的限制,额外的线程只会让情况变得更糟。您需要更好的硬件(例如更快的内存、不同的 CPU、不同的主板)。
你是对的:FPU 是一个主要瓶颈(使用 xs_CRoundToInt 技巧可以让内存总线非常接近饱和)。
以下是 Core 2 (Q6600) 处理器的一些测试结果。这台机器的理论主存带宽为 3.2 GB/s(L1 和 L2 带宽要高得多)。该代码是使用 Visual Studio 2008 编译的。32 位和 64 位的结果相似,并且使用 /O2 或 /Ox 优化。
只写... 1866359 个刻度与 33554432 个数组元素(33554432 触摸)。带宽:1.91793 GB/s 154749 个刻度,262144 个数组元素(33554432 个触摸)。带宽:23.1313 GB/s 108816 个刻度,8192 个数组元素(触摸 33554432 个)。带宽:32.8954 GB/s 使用铸造... 5236122 个刻度与 33554432 个数组元素(33554432 个触摸)。带宽:0.683625 GB/s 2014309 滴答声与 262144 个数组元素(33554432 触摸)。带宽:1.77706 GB/s 1967345 个刻度,包含 8192 个数组元素(33554432 个触摸)。带宽:1.81948 GB/s 使用 xs_CRoundToInt... 1490583 个刻度与 33554432 个数组元素(33554432 触摸)。带宽:2.40144 GB/s 1079530 个刻度,262144 个数组元素(33554432 个触摸)。带宽:3.31584 GB/s 1008407 个刻度,包含 8192 个数组元素(33554432 个触摸)。带宽:3.5497 GB/s
(Windows) 源代码:
// floatToIntTime.cpp : Defines the entry point for the console application.
//
#include <windows.h>
#include <iostream>
using namespace std;
double const _xs_doublemagic = double(6755399441055744.0);
inline int xs_CRoundToInt(double val, double dmr=_xs_doublemagic) {
val = val + dmr;
return ((int*)&val)[0];
}
static size_t const N = 256*1024*1024/sizeof(double);
int I[N];
double F[N];
static size_t const L1CACHE = 128*1024/sizeof(double);
static size_t const L2CACHE = 4*1024*1024/sizeof(double);
static size_t const Sz[] = {N, L2CACHE/2, L1CACHE/2};
static size_t const NIter[] = {1, N/(L2CACHE/2), N/(L1CACHE/2)};
int main(int argc, char *argv[])
{
__int64 freq;
QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
cout << "WRITING ONLY..." << endl;
for (int t=0; t<3; t++) {
__int64 t0,t1;
QueryPerformanceCounter((LARGE_INTEGER*)&t0);
size_t const niter = NIter[t];
size_t const sz = Sz[t];
for (size_t i=0; i<niter; i++) {
for (size_t n=0; n<sz; n++) {
I[n] = 13;
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&t1);
double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
cout << " " << (t1-t0) << " ticks with " << sz
<< " array elements (" << niter*sz << " touched). "
<< "Bandwidth: " << bandwidth << " GB/s" << endl;
}
cout << "USING CASTING..." << endl;
for (int t=0; t<3; t++) {
__int64 t0,t1;
QueryPerformanceCounter((LARGE_INTEGER*)&t0);
size_t const niter = NIter[t];
size_t const sz = Sz[t];
for (size_t i=0; i<niter; i++) {
for (size_t n=0; n<sz; n++) {
I[n] = (int)F[n];
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&t1);
double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
cout << " " << (t1-t0) << " ticks with " << sz
<< " array elements (" << niter*sz << " touched). "
<< "Bandwidth: " << bandwidth << " GB/s" << endl;
}
cout << "USING xs_CRoundToInt..." << endl;
for (int t=0; t<3; t++) {
__int64 t0,t1;
QueryPerformanceCounter((LARGE_INTEGER*)&t0);
size_t const niter = NIter[t];
size_t const sz = Sz[t];
for (size_t i=0; i<niter; i++) {
for (size_t n=0; n<sz; n++) {
I[n] = xs_CRoundToInt(F[n]);
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&t1);
double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
cout << " " << (t1-t0) << " ticks with " << sz
<< " array elements (" << niter*sz << " touched). "
<< "Bandwidth: " << bandwidth << " GB/s" << endl;
}
return 0;
}