【问题标题】:Failure of -mavx optimization with gcc?使用 gcc 优化 -mavx 失败?
【发布时间】: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 中打印消息?)

标签: c gcc mingw avx


【解决方案1】:
This code always results in:
where parameter = n
a[] = {0,0,2, 3, ...,n-2,n-1}
b[] = {n-1, n-1, ... , n-1}
c[] = {n-1, n-2, ... , 0}
when it reaches the above conditions,
then it exits with "OK"

the amount of time spent executing the code 
climbs at an exponential rate
as the value of the parameter is increased

【讨论】:

  • 其实不是,'a'最后是数组{0, 1, ... n-1},因为当next_perm碰到最后一个(也就是减少),它会在返回之前将其反转,因此“a”被重置为其初始状态。正如我所写的那样,我的问题实际上与程序的作用无关:next_perm 按字典顺序查找下一个排列。 rank 计算一个排列在所有排列列表中的位置,而unrank 则相反(从它的位置计算 perm)。既然有n!排列并且程序会枚举所有排列,所花费的时间甚至会以指数级增长。
  • 另外,正如我在问题中解释的那样,我编写了程序来测试 3 个函数 next_permrankunrank 是否正确地完成了它们的工作:它按字典顺序计算所有排列使用 next_perm,并且对于存储在“a”中的每一个,它都会计算它的位置,该位置必须是正确的位置 (0, 1, ... n! - 1),然后从该位置计算回排列,并且它必须等于'a'。我在调用 rank 之前复制了“a”,因为该函数会破坏其输入。
  • 这只是lexicographic order(或字典顺序)的一个例子。对于 n=3,排列为 0 1 2 ; 0 2 1 ; 1 0 2 ; 1 2 0 ; 2 0 1 ; 2 1 0。有 3!=6 个。
猜你喜欢
  • 2011-04-20
  • 2013-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多