获得整数的高半部分和低半部分乘以

时间:2016-02-02 17:40:08

标签: c++ optimization sse multiplication unsigned-integer

我从三个值A,B,C(无符号32位整数)开始。而且我必须获得两个值D,E(也是无符号32位整数)。其中

D = high(A*C);
E = low(A*C) + high(B*C);

我期望两个32位uint的乘法产生64位结果。 "高"和"低"只是我用来标记前32位和最后32位的64位乘法的结果。

我尝试获取一些全功能的优化代码。我在庞大的循环中有一小部分代码,这只是很少的命令行,但是它几乎消耗了所有的计算时间(几小时计算的物理模拟)。这就是为什么我尝试对这个小部分进行优化的原因,其余代码可以保持更多"用户安排良好"。

有一些SSE指令适合计算提到的例程。 gcc编译器可能会优化工作。但是,如果有必要,我不会拒绝直接在SSE intructions中编写一些代码的选项。

请耐心等待我对SSE的低经验。我将尝试象征性地为SSE编写算法。订购面具或理解结构可能会有一些错误。

  1. 按顺序将四个32位整数存储到一个128位寄存器中:A,B,C,C。
  2. 将指令(可能是pmuludq)应用到提到的128位寄存器中,该寄存器将32位整数对相乘并返回64位整数对。因此,它应该同时计算A*C的乘法和B*C的乘法,并返回两个64位值。
  3. 我希望我有新的128位寄存器值P,Q,R,S(四个32位块),其中P,Q是A*C的64位结果,R,S是64位结果B*C。然后我继续在寄存器中重新排列值为P,Q,0,R
  4. 取第一个64位P,Q并加第二个64位0,R。结果是一个新的64位值。
  5. 将结果的前32位读取为D,将结果的最后32位读为E。
  6. 此算法应返回E和D的正确值。

    我的问题:

    c ++中是否有静态代码生成类似于SSE算法的SSE例程?我喜欢更高性能的解决方案。如果算法对标准c ++命令有问题,那么如何在SSE中编写算法呢?

    我使用TDM-GCC 4.9.2 64位编译器。

    (注意:问题在建议后被修改)

    (注2:我对此http://sci.tuomastonteri.fi/programming/sse有一个灵感,就是使用SSE来获得更好的性能)

2 个答案:

答案 0 :(得分:1)

如果我理解正确,您想要计算A * B中潜在溢出的数量。如果是,那么你有两个不错的选择 - "使用两倍大变量" (为uint64写128位数学函数 - 它并不那么难(或者等我明天发布)),并且使用浮点类型": (浮点型(A)*浮子(B))/浮动(C) 因为精度损失是最小的(假设float是4个字节,双8个字节,长16个字节长),float和uint32都需要4个字节的内存(对于uint64_t使用double,因为它应该是8个字节长):

#include <iostream>
#include <conio.h>
#include <stdint.h>

using namespace std;

int main()
{
    uint32_t a(-1), b(-1);
    uint64_t result1;
    float result2;
    result1 = uint64_t(a)*uint64_t(b)/4294967296ull;    // >>32 would be faster and less memory consuming
    result2 = float(a)*float(b)/4294967296.0f;
    cout.precision(20);
    cout<<result1<<'\n'<<result2;
    getch();
    return 0;
}

产地:

4294967294
4294967296

但如果你想要真正准确和正确的答案,我建议使用两倍大的计算类型

现在我想起来了 - 你可以使用long double for uint64和double for uint32而不是为uint64编写函数,但我不认为它保证long double将是128bit,并且你必须检查一下。我会选择更普遍的选择。

编辑:

You can write function to calculate that without using anything more
than A, B and result variable which would be of the same type as A.
Just add rightmost bit of (where Z equals B*(A>>pass_number&1)) Z<<0,
Z<<1, Z<<2 (...) Z<<X in first pass, Z<<-1, Z<<0, Z<<1 (...) Z<<(X-1)
for second (there should be X passes), while right shifting the result
by 1 (the just computed bit becomes irrelevant to us after it's
computed as it won't participate in calculation anymore, and it would
be erased anyway after dividing by 2^X (doing >>X)

(必须放在&#34;代码&#34;因为我在这里是新的,并且无法找到另一种方法来阻止格式化脚本吃掉其中的一半)

这只是一个简单的想法。你必须检查它的正确性(对不起,但我现在真的很累 - 但结果不应该在任何计算点溢出,因为最大值可能有价值如果我是正确的,则为2X,并且算法本身似乎很好。)

如果您仍然需要帮助,我将在明天编写代码。

答案 1 :(得分:1)

除非有多个输入并行处理,否则不需要向量。 clang和gcc已经很好地优化了编写代码的“正常”方式:转换为两倍大小,乘以,然后转换为高半。编译器认识到这种模式。

他们注意到操作数最初为32位,因此在转换为64b之后,上半部分全部为零。因此,他们可以使用x86的mul insn进行32b * 32b-> 64b乘法,而不是进行完全扩展精度64b乘法。在64位模式下,他们使用__uint128_t版本的代码执行相同的操作。

Both of these functions compile to fairly good code (one mul or imul per multiply).gcc -m32不支持128b类型,但我不会介绍它,因为1.你只询问32位值的完全乘法,以及2.当你想要快速运行时,你应该总是使用64位代码。如果你在结果不适合寄存器的情况下进行全乘法,则clang将避免许多额外的mov指令because gcc is silly about this。这个小测试函数为gcc bug报告提供了一个很好的测试用例。

该godbolt链接包含一个在循环中调用它的函数,将结果存储在数组中。它通过一堆混洗自动矢量化,但如果你有多个输入并行处理,它仍然看起来像加速。在乘法后,不同的输出格式可能需要较少的混洗,例如可能为DE存储单独的数组。

我包含128b版本,表明编译器可以处理这个问题,即使它不是微不足道的(例如,只需执行64位imul指令就可以在32位输入上进行64 * 64-> 64b乘法运算,在将任何可能位于函数入口上的输入寄存器中的高位置零后。)

当针对Haswell CPU及更新版本时,gcc和clang可以使用mulx BMI2指令。 (我在godbolt链接中使用-mno-bmi2 -mno-avx2来保持asm更简单。如果你拥有 Haswell CPU,只需使用-O3 -march=haswell。)mulx dest1, dest2, src1 dest1:dest2 = rdx * src1 mul src1 1}}而rdx:rax = rax * src1执行mulx。因此edx有两个只读输入(一个隐式:rdx / mov)和两个只写输出。这使得编译器可以使用较少的mul指令进行全乘,以便将数据输入和输出mulx的隐式寄存器。这只是一个小的加速,尤其是。因为64位// compiles to good code: you can and should do this sort of thing: #include <stdint.h> struct DE { uint32_t D,E; }; struct DE f_structret(uint32_t A, uint32_t B, uint32_t C) { uint64_t AC = A * (uint64_t)C; uint64_t BC = B * (uint64_t)C; uint32_t D = AC >> 32; // high half uint32_t E = AC + (BC >> 32); // We could cast to uint32_t before adding, but don't need to struct DE retval = { D, E }; return retval; } #ifdef __SIZEOF_INT128__ // IDK the "correct" way to detect __int128_t support struct DE64 { uint64_t D,E; }; struct DE64 f64_structret(uint64_t A, uint64_t B, uint64_t C) { __uint128_t AC = A * (__uint128_t)C; __uint128_t BC = B * (__uint128_t)C; uint64_t D = AC >> 64; // high half uint64_t E = AC + (BC >> 64); struct DE64 retval = { D, E }; return retval; } #endif 在Haswell上有4个循环延迟而不是3个循环延迟。 (Strangely, 64bit mul and mulx are slightly cheaper than 32bit mul and mulx。)

{{1}}