【问题标题】:Why is this C function returning int values in two's complement?为什么这个 C 函数以二进制补码形式返回 int 值?
【发布时间】:2018-06-30 01:53:43
【问题描述】:

我正在使用一个库,该库使用英特尔的 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找到

【问题讨论】:

  • 仅供参考the loop instruction is slow on all CPUs other than AMD Bulldozer / Ryzen。也就是说,当 MMX 仍然相关时,所有 CPU 上的速度都很慢,因此编写此代码的人都不知道如何正确优化。大多数编译器应该通过使用 SSE2、AVX2 或 AVX512 自动矢量化 C[i] = A[i] * B[i](对于更广泛的 pmullw 版本)给出良好的结果。
  • ngs.noaa.gov/gps-toolbox/Heckler/Readme.txt 包含非常重要的注释Truncate C to a 16 bit signed integer, preserving sign bit C(0:14) = C(0:14), C(15) = C(31) 看来您应该自己执行此截断
  • @llya Bursov 截断自己更正了问题。

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


【解决方案1】:

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* 并输入错误时会发生这种情况。

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

16 位 pmullw 指令有效地将您的输入数据视为 short(或 unsigned short,因为这是相同的二进制运算)的数组:

// 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,

x86 是 little-endian,所以最不重要的词先出现。我已将 word 和 dword 值以正常的位置值顺序显示为单个十六进制数字,not 以它们在内存中显示为单独的十六进制字节的字节顺序显示。这就是为什么双字 int 的第一个(在内存中)字是 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 自动矢量化 C[i] = A[i] * B[i] 提供良好的结果(对于更广泛的 pmullw 版本)。使用 inline-asm 根本不是一个好主意,使用优化不佳的 MMX asm 是一个更糟糕的主意,除非你真的需要在 Pentium III 或其他不需要的东西上运行它有SSE2。

【讨论】:

  • “您在大多数 32 位元素的最重要(高位)16 位元素中有-1 * -1”是什么意思?
  • @JakeJohnson:每个 32 位元素都由一对高/低 16 位元素组成。 32 位 2 的补码 -1 是 0xffffffff,这也是 16 位 2 的补码 {-1, -1}。
  • 好的,你是说当我得到65537 时,我真的得到了两个16 位元素:{0x001, 0x001}?但是我为什么不直接得到{0x000, 0x001} = 1? {-1, -1} 是否转换为 -1 * -1?我不明白为什么会这样。你能解释一下为什么当-1和1相乘时我得到65535而不是1吗?我认为这将有助于澄清。
  • @JakeJohnson:用更详细的示例更新了我的答案。
猜你喜欢
  • 2020-03-12
  • 2016-05-01
  • 2010-10-13
  • 1970-01-01
  • 2011-10-14
  • 2017-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多