Quantcast
Channel: Big Sky
Viewing all articles
Browse latest Browse all 121

gcc は副作用のある関数呼び出しを含む式評価順序を最適化する。

$
0
0
2013/04/30 タイトル修正

昨日、とある場所でこんな話で盛り上がった。

逆ポーランド計算機を作ろうと思ったんだけど、どうも結果が期待通りにならない。
ソースコードを見せて貰うと以下の様なコードだった。
#include<stdio.h>
#include<stdlib.h>

#define MAX_SIZE 100

int stack[MAX_SIZE];
int stack_pointer = 0;

void push(int data){
  stack[stack_pointer++] = data;
}

int pop(){
  return stack[--stack_pointer];
}

int pop1(int n){
  printf("pop %d\n", n);
  return stack[--stack_pointer];
}


int main(void){
  char s[MAX_SIZE];
  int a, b;

  while( scanf("%s", s) != EOF ){
    switch (s[0]) {
    case '+':
      push(pop() + pop());
      break;
    case '-':
      /*
       * "3 4 -"などを与えると
       * -1ではなく1となってしまう
       * push(-pop() + pop());
       *
       */
      a = pop();
      b = pop();
      push(b - a);

      break;
    case '*':
      push(pop() * pop());
      break;
    default:
      push(atoi(s));
      break;
    }
  }
  printf("%d\n", pop());

  return 0;
}
このコードはちゃんと動作する。足し算と掛け算は間違いなく動作するだろう。でもコメント部に書いてあるコードを有効にすると結果が変わる。
push(-pop() + pop());
もちろん皆さん知ってはいるだろうが、これはスタックから取り出す順に依存する。このコードを書いた人は「3 4 -」という入力を「3 - 4」として処理したいが為に、先に4を取り出して符号を逆転し、3を取り出して加算する事で-1を得るという動作を期待した。
引数の評価順が左から右である事を期待したんですね。
push(-4 + 3);
しかしこれをgccでコンパイルして実行すると結果は -1 ではなく 1 が得られるんです。
なんと gcc は上記のコードを
push(pop() - pop());
に戻してコンパイルしてるんです。試しに
pop1.c
#include <stdio.h>

int a[] = {34}, p = sizeof(a) / sizeof(a[0]);

int
pop() {
  return a[--p];
}

int
main(int argc, char* argv[]) {
  printf("%d\n", -pop() + pop());
  return 0;
}
pop2.c
#include <stdio.h>

int a[] = {34}, p = sizeof(a) / sizeof(a[0]);

int
pop() {
  return a[--p];
}

int
main(int argc, char* argv[]) {
  printf("%d\n", pop() - pop());
  return 0;
}
上記 pop1.c と pop2.c を「gcc -S」でアセンブリ出力して比較して見たら全く同じ出力内容になりました。
    .file   "pop1.c"
.globl a
    .data
    .align 4
    .type   a, @object
    .size   a8
a:
    .long   3
    .long   4
.globl p
    .align 4
    .type   p, @object
    .size   p4
p:
    .long   2
    .text
.globl pop
    .type   pop, @function
pop:
    pushl   %ebp
    movl    %esp, %ebp
    movl    p, %eax
    subl    $1, %eax
    movl    %eaxp
    movl    p, %eax
    movl    a(,%eax,4), %eax
    popl    %ebp
    ret
    .size   pop, .-pop
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    pushl   %ebx
    subl    $28, %esp
    call    pop
    movl    %eax, %ebx
    call    pop
    movl    %ebx, %edx
    subl    %eax, %edx
    movl    $.LC0, %eax
    movl    %edx4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $0, %eax
    addl    $28, %esp
    popl    %ebx
    movl    %ebp, %esp
    popl    %ebp
    ret
    .size   main, .-main
    .ident  "GCC: (GNU4.4.7 20120313 (Red Hat 4.4.7-3)"
    .section    .note.GNU-stack,"",@progbits
最適化オプションは付けていないので、デフォルトの動作としてこうなります。gcc のバージョンは 4.7.2 です。
この最適化をやめるオプションを軽く探してみましたが見つけられませんでした。
これ、実は clang や MSVC だとこの最適化は行われなくて左から評価が行われる為、どちらも結果は -1 となります。
これは僕の推測なのですが、gccはたぶん、-pop+popについて
  1. 左pop
  2. 符号逆転
  3. 右pop
  4. 加算
という処理ををコンパイル時に最適化して pop-pop にする事で
  1. 本来右pop
  2. 本来左pop
  3. 加算
という風に手数を減らしてるんじゃないかと思ってます。
int
main(int argc, char* argv[]) {
  int c = 1;
  printf("%d\n", -pop()*c + pop());
  return 0;
}
こうすれば評価順をちょっとだけ強制できるけど、これも最適化次第では消されてしまうかもしれないしなによりダサい。 もちろんC言語において関数引数の評価順序は規定されていないし、本来こういう呼び出し方はしてはいけないのは皆さん知ってはいると思います。多くの人はこの問題には直面しないでしょう。
C/C++関数引数の評価順序 - yohhoyの日記

プログラミング言語C/C++では、関数実引数の評価順序は未規定(unspecified)となっている。

http://d.hatena.ne.jp/yohhoy/20120304/p1
しかしながら論理演算は左から右に評価される事を期待している為、この様な副作用のあるコードを書くと処理系によっては足をすくわれる事になるという、なんとも面白い事例でした。

気になったので他の言語ではどうなのか調べてみましたが
perl も
my @a = (34);
print ((-pop @a) + (pop @a));
ruby も
a = [34]
puts (-a.pop + a.pop)
python も
a = [34]
print (-a.pop() + a.pop())
javascript も
log = typeof console != 'undefined' ? console.log : print;
var a = [12];
log(-a.pop() + a.pop());
java も
import java.util.Stack;

public class poptest {
    public static void main(String[] args) {
        Stack<Integer> st = new Stack<Integer>();
        st.push(1);
        st.push(2);
        System.out.println(-st.pop() + st.pop());
    }
}
vim script も(誰も聞いてない)
let s:a = [34]
functions:pop()
  let r = s:a[-1]
  let s:a = s:a[:-2]
  return r
endfunction
echo (-s:pop() + s:pop())
-1 でした。
lisp だと
(setq x '(4 3))
(+ (- (pop x)) (pop x))
こう書けば -1 ですかね。

副作用のある処理を式として列挙記述するのはやめましょう。

Viewing all articles
Browse latest Browse all 121

Trending Articles