【发布时间】:2015-03-03 03:39:30
【问题描述】:
编辑 下面的部分解决方案(编辑2),但我还有一个问题(见最后)
我正在尝试使用 gcc-4.9.2 在 Windows 7 上编译以下 C 程序,32 位,在 Pentium G3220 上运行(根据 Windows 系统信息)。如果我理解正确,这个处理器没有 AVX 扩展,所以发生某些事情是很自然的,我只是不确定到底是什么。最初,我正在使用 gcc 进行优化,我尝试了 -mavx 而不是“意外”。
以下程序按字典顺序计算数字 0 ... n-1(以 n 作为参数)的排列,以及每个排列的排名(其在此顺序中的位置),以及“unrank”(恢复排列),并检查所有这些是否正确。最后应该只打印“OK”或“Error”。
- 使用
gcc -O3,程序在我检查的所有整数输入 (1 - 使用
gcc -O3 -mavx,它在 1 = 8,它什么也不打印,实际上它什么也不做(退出前几乎没有延迟)。我没有收到来自程序或 Windows 的消息(我本以为可能会因未知指令而崩溃,但它没有发生)。
(在另一台具有 Windows 7 64 位、core-i5 和相同 gcc-4.9.2 的计算机上,当以 32 或 @ 编译时,该程序似乎在没有 -mavx 的情况下运行良好987654323@)
我不明白为什么它对某些输入值运行正确,而对其他输入值则失败。有人对此有什么暗示吗?
这是完整的程序,后面是一个较短的程序,有同样的问题。
#include <stdlib.h>
#include <stdio.h>
#define SWAP(a,b) {int c; c = a; a = b; b = c;}
int next_perm(int n, int a[n]) {
int i, j, k;
for(i = n - 1; i > 0 && a[i - 1] > a[i]; i--);
for(j = i, k = n - 1; j < k; j++, k--) SWAP(a[j], a[k]);
if(i == 0) return 0;
for(j = i--; a[j] < a[i]; j++);
SWAP(a[i], a[j]);
return 1;
}
#undef SWAP
void copyvec(int n, int dst[n], int src[n]) {
int i;
for(i = 0; i < n; i++) {
dst[i] = src[i];
}
}
int eqvec(int n, int a[n], int b[n]) {
int i;
for(i = 0; i < n; i++) {
if(a[i] != b[i]) return 0;
}
return 1;
}
int rank(int n, int a[n]) {
int v[n], i, j, r;
v[n - 1] = 1;
for(j = n - 2; j >= 0; j--) v[j] = v[j + 1]*(n - 1 - j);
for(r = i = 0; ; i++) {
for(j = i; j < n; j++) {
if(a[j] > j) goto cont;
}
return r;
cont:
i = j;
r += v[i]*(a[i] - i);
for(j = i + 1; j < n; j++) {
if(a[j] < a[i]) a[j]++;
}
}
}
void unrank(int n, int a[n], int p) {
int v[n], i, j, r, s;
v[n - 1] = 1;
for(i = n - 2; i >= 0; i--) v[i] = v[i + 1]*(n - 1 - i);
p %= n*v[0];
for(i = 0; i < n; i++) a[i] = i;
for(i = 0; p > 0; i++) {
for(; v[i] > p; i++);
r = p/v[i];
p %= v[i];
for(s = a[j = i + r]; j >= i; j--) a[j] = a[j - 1];
a[i] = s;
}
}
int main(int argc, char **argv) {
int n, i, r, s = 0, q = 0;
int *a = NULL, *b = NULL, *c = NULL;
if(argc == 2 && (n = strtol(argv[1], NULL, 0)) > 0) {
a = malloc(n*sizeof(int));
b = malloc(n*sizeof(int));
c = malloc(n*sizeof(int));
if(!a || !b || !c) {
puts("Unable to allocate memory");
goto end;
} else {
for(i = 0; i < n; i++) a[i] = i;
do {
copyvec(n, b, a);
r = rank(n, b);
unrank(n, c, r);
q |= s++ != r || !eqvec(n, a, c);
} while(next_perm(n, a));
puts(q?"Error":"OK");
}
} else {
puts("perm n - Check all permutations of {0 ... n - 1}, with n > 0");
}
end:
if(a) free(a);
if(b) free(b);
if(c) free(c);
return 0;
}
编辑
根据 Brian Cain 的评论,这是一个具有相同问题的较短程序。我删除了对输入值的所有检查,所有排名/未排名的东西,我用一个大小为 20 的数组替换了 malloc/free(现在只有一个,因为 b 和 c 不再使用)。现在程序只用while(next_perm(n, a)); 循环计算排列,而不用它们做任何事情。不过,它最后仍应打印“OK”,因为在初始 q=0 之后 q 的值不会改变。
#include <stdlib.h>
#include <stdio.h>
#define SWAP(a,b) {int c; c = a; a = b; b = c;}
int next_perm(int n, int a[n]) {
int i, j, k;
for(i = n - 1; i > 0 && a[i - 1] > a[i]; i--);
for(j = i, k = n - 1; j < k; j++, k--) SWAP(a[j], a[k]);
if(i == 0) return 0;
for(j = i--; a[j] < a[i]; j++);
SWAP(a[i], a[j]);
return 1;
}
#undef SWAP
int main(int argc, char **argv) {
int n, i, r, s = 0, q = 0, a[20];
n = strtol(argv[1], NULL, 0);
for(i = 0; i < n; i++) a[i] = i;
while(next_perm(n, a));
puts(q?"Error":"OK");
return 0;
}
编辑 2:程序集输出说明
我还添加了 gcc 的反汇编输出(采用 Intel 语法),在 gcc -O3 -mavx -S -masm=intel 和 gcc-4.9.2 中找到(有关编译器的实际二进制文件,请参见上面的链接)。但是,它需要一些工作,因为 gcc 将内联对 next_perm 的调用,并且它的可读性较差。我还删除了CFI directives 和对齐以及实际上所有其他指令,以提高可读性:
_next_perm:
LFB0:
push ebp
push edi
push esi
push ebx
mov ecx, DWORD PTR [esp+20]
mov edx, DWORD PTR [esp+24]
lea eax, [ecx-1]
test eax, eax
jle L12
mov edi, DWORD PTR [edx-4+ecx*4]
cmp DWORD PTR [edx-8+ecx*4], edi
mov ecx, eax
jg L5
jmp L11
L28:
mov esi, DWORD PTR [edx+ecx*4]
cmp DWORD PTR [edx-4+ecx*4], esi
jle L27
L5:
sub ecx, 1
jne L28
L4:
mov ebx, ecx
L7:
mov esi, DWORD PTR [edx+ebx*4]
mov edi, DWORD PTR [edx+eax*4]
mov DWORD PTR [edx+ebx*4], edi
mov DWORD PTR [edx+eax*4], esi
add ebx, 1
sub eax, 1
cmp ebx, eax
jl L7
L2:
xor eax, eax
test ecx, ecx
je L23
L11:
sal ecx, 2
lea esi, [edx+ecx]
lea ebp, [edx-4+ecx]
mov ebx, DWORD PTR [esi]
mov edi, DWORD PTR [ebp+0]
cmp edi, ebx
jle L9
lea eax, [edx+4+ecx]
L10:
mov esi, eax
add eax, 4
mov ebx, DWORD PTR [eax-4]
cmp ebx, edi
jl L10
L9:
mov DWORD PTR [ebp+0], ebx
mov eax, 1
mov DWORD PTR [esi], edi
L23:
pop ebx
pop esi
pop edi
pop ebp
ret
L27:
cmp eax, ecx
jg L4
jmp L11
L12:
mov ecx, eax
jmp L2
无论有没有-mavx,汇编输出都一样,除了标签号:没有AVX指令,这意味着问题实际上出在main。
这可以通过在 main 中添加一些 puts 来检查:
int main(int argc, char **argv) {
int n, i, q = 0, a[20];
puts("X");
n = strtol(argv[1], NULL, 0);
puts("Y");
for(i = 0; i < n; i++) a[i] = i;
puts("Z");
while(next_perm(n, a));
puts(q?"Error":"OK");
return 0;
}
然后,程序在失败时只打印 X 和 Y,因此问题来自用于在 Y 和 Z 之间的 for 循环中构建“a”的 AVX 指令。
这是main 的汇编输出,同样没有指令(LC2 指向“Y”,LC3 指向“Z”)。 main 的汇编输出中唯一的 AVX 指令位于这两个 puts 之间,它们用于构建初始 'a' 的 for 循环,即数组 {0, 1, ..., n -1}。实际发生的情况是,AVX 指令一次用于构建“a”的多个元素(我猜是 4 个),如果“a”的长度不是 4 的倍数,那么还有一个额外的步骤(在L4 和 L9),在 L9 调用 puts("Z") 之前,然后调用 while(next_perm(n, a));在 L3。因此,问题很简单:如果n 足够小,那么AVX 循环实际上没有运行,并且没有错误。这里最大有效n 是 4,但它在 gcc 的不同运行之间有所不同,看起来有点随机(我昨天得到了 8 个)。
LC0 和 LC4 标签指向 AVX 指令使用的两个包含 4 个元素的数组:LC0 是 {0,1,2,3},LC4 是 {4,4,4,4}。难怪他们为什么会在这里,即使对 AVX 没有深入的了解,它也闻起来像一个展开的循环:-)
_main:
push ebp
mov ebp, esp
push edi
push esi
push ebx
and esp, -16
sub esp, 96
call ___main
mov DWORD PTR [esp], OFFSET FLAT:LC1
call _puts
mov eax, DWORD PTR [ebp+12]
mov DWORD PTR [esp+8], 0
mov DWORD PTR [esp+4], 0
mov eax, DWORD PTR [eax+4]
mov DWORD PTR [esp], eax
call _strtol
mov DWORD PTR [esp], OFFSET FLAT:LC2
mov ebx, eax
call _puts
test ebx, ebx
jle L17
lea edx, [ebx-4]
lea ecx, [ebx-1]
shr edx, 2
add edx, 1
cmp ecx, 3
lea eax, [0+edx*4]
jbe L10
vmovdqa xmm1, XMMWORD PTR LC4
lea esi, [esp+16]
xor ecx, ecx
vmovdqa xmm0, XMMWORD PTR LC0
L5:
mov edi, ecx
add ecx, 1
sal edi, 4
cmp edx, ecx
vmovaps XMMWORD PTR [esi+edi], xmm0
vpaddd xmm0, xmm0, xmm1
ja L5
cmp ebx, eax
je L9
L4:
lea edx, [eax+1]
mov DWORD PTR [esp+16+eax*4], eax
cmp ebx, edx
jle L9
mov DWORD PTR [esp+16+edx*4], edx
lea edx, [eax+2]
cmp ebx, edx
jle L9
add eax, 3
mov DWORD PTR [esp+16+edx*4], edx
cmp ebx, eax
jle L9
mov DWORD PTR [esp+16+eax*4], eax
L9:
mov DWORD PTR [esp], OFFSET FLAT:LC3
call _puts
L3:
mov DWORD PTR [esp+4], esi
mov DWORD PTR [esp], ebx
call _next_perm
test eax, eax
jne L3
mov DWORD PTR [esp], OFFSET FLAT:LC5
call _puts
lea esp, [ebp-12]
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
ret
L10:
xor eax, eax
lea esi, [esp+16]
jmp L4
L17:
lea esi, [esp+16]
jmp L9
现在,我了解了实际发生的情况,但仍然存在一个问题:为什么当程序尝试运行 AVX 指令时没有任何错误消息?它只是退出,或者被杀死,但没有任何迹象表明出了什么问题。
【问题讨论】:
-
“我不知道如何使它更短,同时仍然表现出这种奇怪的行为”——我建议将示例一分为二,与良好情况下的中间预期结果进行比较。
-
@user3629249 不,'i' 的值有一次变为 0(当最后一个排列被命中时:整个 'a' 正在减少)。由于之后使用了'i',我不知道你怎么能想象循环什么都不做:它的唯一目的是找到'i'。也许困扰你的是'for'后面的分号。 C 允许这样做。
-
@user3629249 您可能知道,
for(a;b;c) d完全等同于a; while(b) { d; c; }。 for 循环中可能缺少 "a"、"b"、"c" 中的任何一个,并且 "d" 可能会缩减为单个分号。 -
W.r.t.你的最后一个问题:简单。在每条 SSE/AVX 指令之前进行运行时检查以验证 CPU 是否支持其指令集将是令人发指的(而且速度很慢)。您对
-mavx的使用承诺该二进制文件将仅在支持 AVX 的 CPU 上运行;如果不是这种情况,x86 CPU 会引发异常#UD(未定义的操作码),操作系统会通过向有罪进程提供SIGILL(非法指令)信号来对此作出反应,其默认操作是中止。跨度> -
@IwillnotexistIdonotexist 实际上,它必须如此简单。快速检查显示,即使除以 0 或取消引用空指针,也会静默中止。我以为默认有消息,可能是因为我过去的 Linux 经验(我猜他们会在 Linux 中打印消息?)