为什么这个 C 函数以二进制补码返回 int 值?
Why is this C function returning int values in two's complement?
我正在使用一个库,该库使用 Intel 的 MMX 单指令、多数据 (SIMD) 指令集来加速整数数组的乘法。我正在使用的函数包含内联汇编,以使用 Intel 处理器中的 MMX SIMD 寄存器并执行乘法。
将两个整数数组与该函数相乘后,我收到一个数组,其中包含错误的整数值,本应为负数。但是,当将这些值转换为二进制时,我注意到整数以 2 的补码表示正确的值。整数应该是 16 位长。
更奇怪的是,当两个负整数相乘时,而不是一个正数和一个负数,函数 returns 一个整数值,当转换为二进制时,添加一个额外的位作为最高有效位(将附加位标记到二进制数的左侧)。该位的值为 1,但如果忽略该位,其余位将正确显示预期值。
这很难用语言表达,所以让我举个例子:
我有三个整数数组 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 标志的英特尔 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
找到
pmullw
乘以压缩整数 words(Intel 术语中的 16 位元素)。 int
是一种 32 位类型,为此您需要 SSE4.1 pmulld
(打包的双字)(或者使用 SSE2 pmuludq
进行一些改组以仅保留每个 64 位的低半部分结果)。
and an example of me calling it.
int A[8] = {-1, 4, 1, -1, 1, -2, -3, 7};
您向它传递了 32 位整数,但您已经说过您知道它需要 16 位整数。 (int
是所有主要 32 位和 64 位 x86 调用约定/ABI 中的 32 位类型)。 当您使用 void*
并弄错类型时会发生这种情况。
你的 65537
来自 -1
和 -1
很容易解释:它是 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 是小端字节序,所以最不重要的字排在最前面。我已经将正常位值顺序中的字和双字值显示为单个十六进制数字, 而不是 中的字节顺序它们在内存中显示为单独的十六进制字节。这就是为什么双字 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(对于 pmullw
的更宽版本)自动向量化 C[i] = A[i] * B[i]
来获得良好的结果。使用 inline-asm 根本不是一个好主意,使用优化不佳的 MMX asm 是一个更糟糕的主意,除非你真的需要 运行 在 Pentium III 上这样做或者其他没有 SSE2 的东西。
我正在使用一个库,该库使用 Intel 的 MMX 单指令、多数据 (SIMD) 指令集来加速整数数组的乘法。我正在使用的函数包含内联汇编,以使用 Intel 处理器中的 MMX SIMD 寄存器并执行乘法。
将两个整数数组与该函数相乘后,我收到一个数组,其中包含错误的整数值,本应为负数。但是,当将这些值转换为二进制时,我注意到整数以 2 的补码表示正确的值。整数应该是 16 位长。
更奇怪的是,当两个负整数相乘时,而不是一个正数和一个负数,函数 returns 一个整数值,当转换为二进制时,添加一个额外的位作为最高有效位(将附加位标记到二进制数的左侧)。该位的值为 1,但如果忽略该位,其余位将正确显示预期值。
这很难用语言表达,所以让我举个例子:
我有三个整数数组 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 标志的英特尔 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
找到pmullw
乘以压缩整数 words(Intel 术语中的 16 位元素)。 int
是一种 32 位类型,为此您需要 SSE4.1 pmulld
(打包的双字)(或者使用 SSE2 pmuludq
进行一些改组以仅保留每个 64 位的低半部分结果)。
and an example of me calling it.
int A[8] = {-1, 4, 1, -1, 1, -2, -3, 7};
您向它传递了 32 位整数,但您已经说过您知道它需要 16 位整数。 (int
是所有主要 32 位和 64 位 x86 调用约定/ABI 中的 32 位类型)。 当您使用 void*
并弄错类型时会发生这种情况。
你的 65537
来自 -1
和 -1
很容易解释:它是 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 是小端字节序,所以最不重要的字排在最前面。我已经将正常位值顺序中的字和双字值显示为单个十六进制数字, 而不是 中的字节顺序它们在内存中显示为单独的十六进制字节。这就是为什么双字 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(对于 pmullw
的更宽版本)自动向量化 C[i] = A[i] * B[i]
来获得良好的结果。使用 inline-asm 根本不是一个好主意,使用优化不佳的 MMX asm 是一个更糟糕的主意,除非你真的需要 运行 在 Pentium III 上这样做或者其他没有 SSE2 的东西。