为什么这个C函数以二进制补码返回int值?

时间:2018-01-21 07:20:13

标签: c simd inline-assembly twos-complement mmx

我正在使用一个使用Intel MMX单指令,多数据(SIMD)指令集的库来加速整数数组的乘法运算。我正在使用的函数包含内联汇编,以便在Intel处理器中使用MMX SIMD寄存器并执行乘法。

在将两个整数数组与函数相乘后,我收到一个数组,该数组包含错误的整数值,该值应为负数。但是,当将这些值转换为二进制时,我注意到整数表示2的补码中的正确值。整数应该是16位长。

更奇怪的是,当两个负整数相乘时,而不是一个正的负数,该函数返回一个整数值,当转换为二进制时,将一个额外的位添加为最高位(将附加位标记到左侧)二进制数的一面)。该位的值为1,但如果忽略该位,则其余位正确显示预期值。

用语言很难说,所以让我举一个例子:

我有三个int数组A,B和C.

A = {-1,4,1,-1,1,-2,-3,7},

B = {-1,-1,-1,-1,-1,-1,-1,1}

C = {0,0,0,0,0,0,0,0}

当A和B相乘时,我希望

{1,-4,-1,1,-1,2,3,7}

存储在C。

然而,在使用了库的功能之后,我得到了

{65537,65532,65535,65537,65535,65538,65539,7}

作为我的C值。

第一个值65537,二进制为10000000000000001。没有额外的第17位,这将等于1,但即便如此,该值应该只是1而不是65537.第二个值65532,二进制是1111111111111100 -4是2的补码。这很好,但为什么这个价值不仅仅是-4。还要注意最后一个值,7。当不涉及负号时,函数会给出预期形式的值。

内嵌程序集是为在Microsoft Visual Studio上编译而编写的,但我使用的是带有-use-msasm标志的intel的c / c ++编译器。

这是功能代码:

void mmx_mul(void *A, void *B, void *C, int cnt)
{

int cnt1;
int cnt2;
int cnt3;

cnt1 = cnt / 32;
cnt2 = (cnt - (32*cnt1)) / 4;
cnt3 = (cnt - (32*cnt1) - (4*cnt2));


__asm
{

    //; Set up for loop
    mov edi, A; // Address of A source1
    mov esi, B; // Address of B source2
    mov ebx, C; // Address of C dest
    mov ecx, cnt1;  // Counter
    jecxz ZERO;

    L1:

        movq mm0, [edi];        //Load from A
        movq mm1, [edi+8];      //Load from A
        movq mm2, [edi+16];     //Load from A
        movq mm3, [edi+24];     //Load from A
        movq mm4, [edi+32];     //Load from A
        movq mm5, [edi+40];     //Load from A
        movq mm6, [edi+48];     //Load from A
        movq mm7, [edi+56];     //Load from A

        pmullw mm0, [esi];      //Load from B & multiply B * (A*C)
        pmullw mm1, [esi+8];    //Load from B & multiply B * (A*C)
        pmullw mm2, [esi+16];   //Load from B & multiply B * (A*C)
        pmullw mm3, [esi+24];   //Load from B & multiply B * (A*C)
        pmullw mm4, [esi+32];   //Load from B & multiply B * (A*C)
        pmullw mm5, [esi+40];   //Load from B & multiply B * (A*C)
        pmullw mm6, [esi+48];   //Load from B & multiply B * (A*C)
        pmullw mm7, [esi+56];   //Load from B & multiply B * (A*C)

        movq [ebx],    mm0;     //Store C = A*B
        movq [ebx+8],  mm1;     //Store C = A*B
        movq [ebx+16], mm2;     //Store C = A*B
        movq [ebx+24], mm3;     //Store C = A*B
        movq [ebx+32], mm4;     //Store C = A*B
        movq [ebx+40], mm5;     //Store C = A*B
        movq [ebx+48], mm6;     //Store C = A*B
        movq [ebx+56], mm7;     //Store C = A*B

        add edi, 64;
        add esi, 64;
        add ebx, 64;

    loop L1;                            // Loop if not done

ZERO:

    mov ecx, cnt2;
    jecxz ZERO1;

    L2:

        movq mm1, [edi];        //Load from A
        pmullw mm1, [esi];      //Load from B & multiply B * (A*C)
        movq [ebx], mm1;
        add edi, 8;
        add esi, 8;
        add ebx, 8;

    loop L2;

ZERO1:

    mov ecx, cnt3;
    jecxz ZERO2;

    mov eax, 0;


    L3:                             //Really finish off loop with non SIMD instructions

        mov eax, [edi];
        imul eax, [esi];
        mov [ebx], ax;
        add esi, 2;
        add edi, 2;
        add ebx, 2;

    loop L3;

ZERO2:

    EMMS;

}


}

以及我呼唤它的一个例子。

int A[8] = {-1, 4, 1, -1, 1, -2, -3, 7};
int B[8] = {-1, -1, -1, -1, -1, -1, -1, 1};
int C[8];
mmx_mul(A, B, C, 16);

最后一个参数16是A和B组合的总元素数。

我使用的图书馆是免费使用的,可以在https://www.ngs.noaa.gov/gps-toolbox/Heckler.htm

找到

1 个答案:

答案 0 :(得分:2)

pmullw将打包的整数(英特尔术语中的16位元素)相乘。 int是一个32位类型,你需要SSE4.1 pmulld(打包dword)(或者用SSE2 pmuludq进行一些改组,只保留每个64位的低半部分结果)。

  

以及我呼唤它的一个例子。

int A[8] = {-1, 4, 1, -1, 1, -2, -3, 7};

你传递了32位整数,但你已经说过你知道它需要16位整数。 (int是所有主要32位和64位x86调用约定/ ABI中的32位类型。 当您使用void*并使类型错误时会发生这种情况。

来自65537-1的{​​{1}}很容易解释:来自两个打包的16位,它是2 ^ 16 + 1,即-1 0x001001。在大多数32位元素的最重要(上部)16位元素中有-1 * -1 = 1

16位-1 * -1指令有效地将输入数据视为pmullw(或short的数组,因为它是相同的二进制操作):

unsigned short

x86是little-endian,所以最不重要的词首先出现。我以正常的位值顺序将单词和双字值显示为单个十六进制数,,它们在字节顺序中作为单独的十六进制字节出现在内存中。这就是为什么双字// 32-bit value -1 = 0xFFFFFFFF 4 1 short A[] = { 0xFFFF, 0xFFFF, 0x0004, 0x0000, 0x0001, 0x0000, ... } // 32-bit value: -1, -1, -1 short B[] = { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, ... } short C: 0x0001, 0x0001, 0xFFFC, 0, 0xFFFF, 0 // 32-bit value: 0x00010001 0x0000FFFC 0x0000FFFF // 65537, 65532, 65535, 的第一个(内存中)字是int值的低16位。

另请参阅https://en.wikipedia.org/wiki/Two%27s_complement以获取有关x86(以及基本上所有其他现代CPU架构)上有符号整数的位表示的更多背景信息。

仅供参考the loop instruction is slow on all CPUs other than AMD Bulldozer / Ryzen。当MMX仍然相关时,它在所有CPU上都很慢,所以编写这段代码的人都不知道如何正确优化。

大多数编译器都应该通过SSE2,AVX2或AVX512自动向量化int 来提供良好的结果(对于C[i] = A[i] * B[i]的更广泛版本)。完全使用inline-asm并不是一个好主意,并且使用经过严格优化的 MMX asm是一个更糟糕的想法,除非你真的需要在Pentium III上运行它,或者其他不需要的东西。 ;有SSE2。