연산자와 컴파일러의 심정 증가

30624 단어 CGCCclangLLVMtech

개시하다


갑작스럽지만 수수께끼다.다음 코드의 실행 결과는 어떻게 될까요?
#include <stdio.h>

int main(){
  int a = 1;
  int b = ++a + ++a + ++a;
  printf("%d\n",b);
}
결과는 처리 시스템에 달려 있다. gcc는 10이고 cling은 9이다.이것은 정의되지 않은 동작인지 처리 시스템의 정의인지, 아니면 규격경찰에게 의뢰한 것인지, gcc와clang이 어떻게 설명했는지 이런 심정을 알아보고 싶습니다.
또한 다음은 중간 코드와 다른 제가 임의로 컴파일러의 심정을 추측하여 정확성을 보장하지 못합니다.

증가 연산자

++의 증가 연산자는 변수에 1을 더한다.예를 들어++aa++ 등이 앞과 뒤에 설치된 경우 대입 행위에 변화가 발생할 수 있다.
앞에 놓으면 증량이 대입되기 전에 진행된다.예컨대
int a = 1;
int b = ++a;

상당
int a = 1;
a = a + 1
int b = a;
.그래서b=2.
뒤에 붙일 때 증량은 대입 후에 진행된다.그러므로
int a = 1;
int b = a++;
int a = 1;
int b = a;
a = a + 1
, 즉b=1에 해당한다.
그러면 이 점증 연산자는 종종 문제를 일으킨다.예를 들어 덧셈의 양측에 나타난 상황에서 그 해석은 애매성을 나타낸다.
아래의 예를 고려해 보자.
int a = 1;
int b = ++a + ++a;
이때b의 값은 얼마입니까?이때 gcc와clang의 의견은 이미 다르다.gcc는 6,clang은 5로 해석한다.이게 어떻게 된 일인지 보자, 이게 이 보도의 목적이야.

clang 기분


clang의 기분을 보려면 LLVM의 중간 코드를 보는 것이 매우 빠르다.
예를 들어 이런 코드를 써 보아라.
int func(int a){
  return ++a;
}
이 LLVM 중간 코드를 보여 주세요.
clang -emit-llvm -S test.c
이렇게 하면test.ll돼요. 안에 있는 것 좀 볼게요.
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @func(i32 %0) #0 {
  %2 = alloca i32, align 4
  store i32 %0, i32* %2, align 4
  %3 = load i32, i32* %2, align 4
  %4 = add nsw i32 %3, 1
  store i32 %4, i32* %2, align 4
  ret i32 %4
}
뒤죽박죽이지만 천천히 보면 임시 변수에 증량 연산자가 있고 대입 시 임시 변수의 값을 사용한다는 것을 알 수 있다.즉
int b = ++a;

해석
int tmp = a + 1;
a = tmp;
int b = tmp;
.이 점을 잘 아는 것은 LLVM의 마지막store이다.아까 LLVM이 C 로 썼어요.
int func(int a){
  int tmp = a + 1;
  a = tmp;
  return tmp;
}
.사용하지 않았지만a 대입문a=tmp이 하나 있다.그것에 대응하는 것은 store i32 %4, i32* %2, align 4이다.
그렇다면++aint tmp=a+1;a=tmp로 전환된 것을 알았다면 클랑의'심정'을 이해할 수 있었을 것이다.
다음 코드를 생각해 보세요.
int a = 1;
int b = ++a + ++a;
++a는 임시 변수tmp = a + 1;로 전환되었고 ++a를 값으로 사용했다. 이런 규칙에 따라 먼저 왼쪽tmp은 이렇게 전환되었다.
int a = 1;
int tmp1 = a + 1;
a = tmp1;
int b = tmp1 + ++a;
오른쪽++a도 마찬가지로 바뀐다.
int a = 1;
int tmp1 = a + 1;
a = tmp1;
int tmp2 = a + 1;
a = tmp2;
int b = tmp1 + tmp2;
이 동작을 계속하면 ++a,tmp1=2였기 때문에 결과는tmp2=3였다.
완전히 같다
int a = 1;
int b = ++a + ++a + ++a;

int a = 1;
int tmp1 = a + 1; // tmp1 = 2
a = tmp1;
int tmp2 = a + 1; // tmp2 = 3
a = tmp2;
int tmp3 = a + 1; // tmp3 = 4
a = tmp3;
int b = tmp1 + tmp2 + tmp3;
로 전환하면 답은2 + 3 = 5이다.이것이 바로 시작 코드의 실행 결과가 클릭에서 9인 이유이다.

GCC의 기분


이제 GCC의 기분을 알아보자.나는 이런 코드를 쓸 줄 안다.
int func(int a){
  return ++a + ++a;
}
gcc에 2 + 3 + 4 = 9 옵션을 추가하여 컴파일하면 컴파일링이 코드를 어떻게 해석하는지 알 수 있습니다.
gcc- c -fdump-tree-all test.c
서류는 뒤죽박죽이 될 수 있지만 볼 것은-fdump-tree-all이다.
func (int a)
{
  int D.1914;

  a = a + 1;
  a = a + 1;
  D.1914 = a * 2;
  return D.1914;
}
알기 쉽게 다시 쓰는 게 그런 건가.
int func(int a){
  a = a + 1;
  a = a + 1;
  int tmp = a + a;
  return tmp;
}
즉, GCC는 먼저 test.c.005t.gimple 양측에 나타난 점증 연산자를 처리한 다음에 덧셈을 집행한다.그러므로
int a = 1;
int b = ++a + ++a;

다시 써야 하기 때문에
int a = 1;
a = a + 1;
a = a + 1;
tmp = a + a
int b = tmp;
써야 한다+.
그럼 3층은 어때요?이런 코드를 쓰세요.
int func(int a){
  return ++a + ++a + ++a;
}
이 추가b=6를 컴파일합니다.
gcc -c -fdump-tree-all test.c
서류도 많이 만들 수 있지만 먼저 -fdump-tree-all보세요.
{
  return ( ++a +  ++a) +  ++a;
}
왼쪽test.c.004t.original을 먼저 처리한 것 같아요.아래를 보시오+.
func (int a)
{
  int D.1914;

  a = a + 1;
  a = a + 1;
  _1 = a * 2;
  a = a + 1;
  D.1914 = a + _1;
  return D.1914;
}
조금만 정리하면 이렇게 돼요.
int func(int a){
  a = a + 1;
  a = a + 1;
  int tmp1 = a + a;
  a = a + 1;
  int tmp2 = tmp1 + a;
  return tmp2;
}
어떻게 이렇게 됐는지 추측해보자.원래 코드는 이렇습니다.
int func(int a){
  return ++a + ++a + ++a;
}
우선, 컴파일러는 왼쪽의 덧셈을 실행한다.
int func(int a){
  return (++a + ++a) + ++a;
}
왼쪽에 임시 변수test.c.005t.gimple를 받아들인다.
int func(int a){
  int tmp1 = ++a + ++a;
  return tmp1 + ++a;
}
tmp1는 덧셈 전에 두 개의 증량 연산자를 해결했다.
int func(int a){
  a = a + 1;
  a = a + 1;
  int tmp1 = a + a;
  return tmp1 + ++a;
}
다음++a + ++a, 임시 변수tmp1 + ++a를 먼저 받아들인다.
해결
int func(int a){
  a = a + 1;
  a = a + 1;
  int tmp1 = a + a;
  int tmp2 = tmp1 + ++a;
  return tmp2;
}
tmp2의 점증 연산자.
int func(int a){
  a = a + 1;
  a = a + 1;
  int tmp1 = a + a;
  a = a + 1;
  int tmp2 = tmp1 + a;
  return tmp2;
}
이상, GCC
int a = 1;
int b = ++a + ++a + ++a;
int a = 1;
a = a + 1;
a = a + 1;          // a = 3
int tmp = a + a;    // tmp = 3 + 3
a = a + 1;          // a = 4
int tmp2 = tmp + a; // 3 + 3 + 4
int b = tmp2;
, 즉tmp1 + ++a로 해석된다.이것이 바로 시작 코드의 실행 결과가 gcc에서 10인 이유이다.

총결산


현재 증량 연산자가 가법의 양측에 나타날 때 그 결과(또는 해석)는 처리 시스템에 달려 있고 gcc와clang이 각각 어떻게 해석하는지 이런 심정을 추측해 보자.
또 증량 연산자가 있는 언어b = 3 + 3 + 4 = 10에서 a=1의 값이 어떻게 되는지 clang파가 많았지만 Perl은 GCC와 마찬가지로 10개를 더 냈다.
  • clang파(결과 9)
  • PHP
  • D 언어
  • JavaScript
  • GCC Partner(결과 10)
  • Perl
  • 증량 연산자는 분쟁의 원인일 수 있고, 사용하지 않는 언어도 많다(Ruby와 Python 등).
    또 고처럼 전치 증량 없이 후치 증량만 후치 증량으로 하고 심지어는'문'으로 되돌아오지 않는 언어도 있다.그러므로
    a++
    
    지원
    b = a++
    
    등은 대입할 수 없다.함수 매개 변수에 증가 연산자를 입력하면 문제가 발생할 수 있습니다.예를 들어 이런 코드를 고려해 봅시다.
    #include <stdio.h>
    
    void func(int a, int b, int c){
      printf("%d %d %d\n",a ,b ,c);
    }
    
    int main(){
      int i = 1;
      func(i++, i++, i++);
    }
    
    gcc에서 컴파일하더라도 x86에서는'321', ARM에서는'1223'이다.
    Go처럼'문'으로 쓰면 함수의 매개 변수를 삽입할 수 없기 때문에 이 문제가 발생하지 않는다.
    따라서 증량 연산자는 많은 번거로움을 주의해야 한다.개인적으로는'채용은 채용이지만 글로 쓴다'는 Go 가이드라인이 가장 적합하다고 생각한다.

    좋은 웹페이지 즐겨찾기