x+=a가 x=x+a보다 빠른 이유

mrx@Google의 이미지

최근에 x+=a가 x=x+a보다 빠르다는 이야기를 들었는데 왜 빠른지 설명해주실 수 있나요?

둘다 메모리에서 x와 a 값을 가져와서 연산을 한 결과를 다시 x에 저장하는건 알겠는데 이부분만 놓고 보면 속도가 똑같아야 된다고 생각했거든요.

제가 모르는 더 깊숙한 내용이 있는거 같네요.

익명 사용자의 이미지

질문을 파악하는 데 필요한 핵심적인 정보가 없습니다.

1. 무슨 언어인가요
2. x와 a는 어떤 종류(타입)의 객체인가요.

mrx@Google의 이미지

x와 a 둘다 int요

익명 사용자의 이미지

그럼 더 빠를 이유가 없습니다.

어디서 누구에게 그런 이야기를 들으셨는지 무척 궁금하군요.

mrx@Google의 이미지

x+=a 하면 x=x+a랑 의미는 같지만 x+a한 결과값을 x에 다시 저장할때 x를 메모리에서 찾을 필요가 없다고 하셨던거 같네요. 그래서 최적화 면에서 연산속도가 더 빠르다는게 결론이었던거 같은데 근데 어떻게 x를 알아서 찾아가서 저장하는지를 모르겠어요. 이유를 생각해보면 처음에 값만 가져오는게 아니라 변수 위치까지 가져오는거 말곤 설명이 안되네요.

익명 사용자의 이미지

안 빠릅니다.

교수님이 요즘 컴파일러 수준이 어느 정도인지 모르시는 거에요. 교수님이 본인 배우던 시절로 생각하셨다면 그렇게 생각할 수 있는데요. 요즘도 아니고 x+=a 이 정도면 최적화 컴파일러라는 말 나오기도 전의 수준이죠.

익명 사용자의 이미지

요즘 컴파일러는 명시적으로 최적화를 꺼도 (e.g., gcc의 -O0) x += a;x = x + a는 똑같은 바이나리가 나옵니다. 최소한 제가 테스트한 것들은 그렇던데, 혹시 다른 결과를 내놓는 컴파일러가 있다면 제보 바랍니다.

시맨틱이 너무 낮은 수준에서 똑같습니다. 아무리 최적화를 명시적으로 끈다고 해도, 위 두 가지 코드를 구분해서 서로 다른 출력 바이나리가 나오게 할 만한 건덕지가 없는 것이죠.

Stephen Kyoungwon Kim@Google의 이미지

그리고 구현 거의 모르거나 제대로 몰라도 박사 학위 따고 교수 되는 데는 별 문제가 없습니다. 그러다 보니 박사/교수 중에 구현 제대로 모르는 사람이 부지기수입니다. 교수 말이 그나마 신빙성 있는 건 그 교수의 연구 분야와 그 주변 아카데믹한 주제죠. 구현으로 넘어가면 잘하는 교수는 잘 하고 아닌 교수는 하나도 모르는 수준일 수도 있습니다.

x += a와 x = x + a, 그것도 a가 상수인데 이게 프론트 엔드 AST 레벨에서 같지 않을 이유를 전 도저히 모르겠네요.

익명 사용자의 이미지

저도 90년대에 선배님인가 교수님인가.. 아무튼 그런 얘기 들었습니다. 이유가 컴파일러에서 최적화를 더 잘해준다고 그랬던거 같네요. 요새는 어떨지 모르겠네요

익명 사용자의 이미지

++, -- 연산도 마찬가지 이유로
a = a + 1; 보다 a++; 이 더 빠르다고 90년대에 배웠습니다. 요새는 어떨지 모르겠네요

익명 사용자의 이미지

마찬가지 이유로 a = a + 1 보다 a++ 가 더 빠르다고 90년대에 배웠습니다. 지금은 어떨런지 모르겠습니다.
정말 그러한지 알려면

int main (int argc, char **argv)
{
  int a = 0;
  a = a + 1;
  return a;
}

int main (int argc, char **argv)
{
  int a = 0;
  a++;
  return a;
}

질문자님의 의문은 컴파일러가 생산한 기계어(어셈블리) 코드를 비교해봐야 풀리겠네요.
사실상 속도 차이는 거의 없으리라 봅니다.

Stephen Kyoungwon Kim@Google의 이미지

x = x + 1; 과 x += 1; 이 다르다는 건 전혀 이해할 수 없는 얘기고, a = a + 1;과 a++;은 AST 수준에서는 다를 수도 있습니다. 실제로 LLVM + clang++을 이용해서 AST를 덤프해 보면 위의 두 코드는 다르게 나와요.

제 생각에 이유는 최적화가 아니라 소스 코드에서의 의미가 다르기 때문인 것 같습니다.

int x = 10;
auto y = x++ + 1; 

이 경우 두 번째 라인의 +가 sequence point가 아니기 때문에 x++이 +에 비해 어떤 시점에서 완료될지는 undefined입니다. y는 11이 될 수도, 12가 될 수도 있죠.

그런데 x++ 대신 x = x+1;이 y가 있는 라인 하나 위에 왔다면, y는 12가 될 것이 보장되겠죠.

컴파일러 구현자 입장에서는 여러 가지 선택이 가능할 겁니다--이게 중요한지 아닌지, 특히나 최적화의 관점에서 어떤지는 다소 회의적입니다만. 첫째, 그냥 x++을 프론트 엔드 수준에서 x += 1로 정의해 주는 겁니다. 즉, x++을 즉시 처리하는 거죠. 이건 이후 소프트웨어 아키텍쳐/알고리즘을 단순화하는 데 도움을 줄 거라고 생각합니다.

아니면 x++과 x += 1을 AST 수준에서는 구분해서 적어도 혹시 모를 최적화 기회를 여전히 열어두는 것이고요. 이게 어떻게 해석될지는 IR 최적화 모듈과 코드 생성기에 달리게 될 것 같습니다. 제시하신 코드의 경우 두 가지가 사실상 차이는 없기 때문에 AST는 달라도 IR부터는 같으리라 예상됩니다만, 좀더 복잡한, 제가 생각하지 못하는 예가 있을 순 있겠죠.

뭔가 성능이 다르다면, 그건 소스 코드 상에서의 의미가 다르기 때문일 가능성이 클 것입니다. 소스 코드 상의 의미가 완전히 동일하다면 (제시하신 두 코드), AST는 다를 수 있어도 IR과 생성된 코드는 같을 겁니다.

clang++ + llvm 10으로 테스트 해보니 AST는 postfix ++ 연산자가 있고, LLVM IR에서는 두 코드가 동일하게 보입니다. 당연히 바이너리도 동일할 겁니다. (입력이 동일하고 코드 생성기는 deterministic한 알고리즘이라는 가정 하에)

속도 차이가 없을 거라는 데는 저도 동의합니다. 만약 AST 수준에서 postfix ++ 연산자가 남아서 코드 생성기까지 전달이 된다고 가정한다면, 그리고 몇몇 임베디드 프로세서들 (요즘도 그런 명령어가 있는지는 모르겠으나 2천년대 초반엔 그런 게 있었다고 들었습니다)처럼 post increment를 하는 명령어가 따로 있다면, 코드 생성기가 쉽게 그 명령어를 사용해서 코드 생성이 가능하겠죠.

그런데, 그 코드의 패턴이 엄청나게 복잡한 것도 아니고 기껏 x=x+1 입니다. 이걸 찾아내지 못하는 decent한 코드 생성기를 저는 지금으로서는 생각하기 어렵네요. 차라리 x = x + 1쪽이 컴파일러가 IR optimizer가 다른 최적화를 더 유연하게 적용할 기회가 있을 수 있어서, 손쉽게 반드시 post increment AST 연산자를 IR을 지나 코드 생성기까지 들고 가는 게 반드시 유리하다고 볼 수 있는지도 확신이 어렵습니다.

세벌의 이미지

그런 얘기 한 사람에게 물어보는 건 어때요?

x+=a 4글자, x=x+a 5글자 그래서 1글자 읽는 만큼 미묘한 차이? 저도 잘 모르겠네요.

익명 사용자의 이미지

학생 입장에서 하늘같은 교수님이라, 항상 바쁘신 분이라 질문하기가 어려워서 이곳에 질문한거 같네요.

익명 사용자의 이미지

#include <stdio.h>
#include <time.h>
 
#define COUNT 1000000000
 
void run1()
{
        clock_t start,end;
        int a=0;
        int x=0;
        int i;
        a=1;
        x=10;
        start=clock();
        for(i=0;i<COUNT;i++)
        {
                x=x+a;
        }
        end=clock();
        printf("Time : x=x+a\t%lf\n",(double)(end - start)/CLOCKS_PER_SEC);
}
void run2()
{
        clock_t start,end;
        int a=0;
        int x=0;
        int i;
        a=1;
        x=10;
        start=clock();
        for(i=0;i<COUNT;i++)
        {
                x+=a;
        }
        end=clock();
        printf("Time : x+=a\t%lf\n",(double)(end - start)/CLOCKS_PER_SEC);
}
 
 
 
int main()
{
        run2();
        run1();
        return 0;
}

결과

root@ip-172-31-5-82:~# gcc a.c -o a
root@ip-172-31-5-82:~# ./a
Time : x+=a     2.786617
Time : x=x+a    2.695882
root@ip-172-31-5-82:~# ./a
Time : x+=a     2.742767
Time : x=x+a    2.681693
root@ip-172-31-5-82:~# ./a
Time : x+=a     2.738263
Time : x=x+a    2.699354
root@ip-172-31-5-82:~# ./a
Time : x+=a     2.764878
Time : x=x+a    2.712889

전 x=x+a가 더 빠른디요... 왜이런지 저도 설명을 듣고 싶네요

Stephen Kyoungwon Kim의 이미지

gcc version 따라 다를 가능성이 아예 없진 않지만, -fdump-tree-optimized 옵션을 -c 주고 컴파일 할 때 주시고 코드를 비교해 보세요. run1과 run2를 분리해서 따로 컴파일하시면 보기 더 좋겠죠. gimple tree를 다른 파일에 덤프합니다. 비교해 보니 정확히 같은 코드고, 코드 생성기는 정확히 같은 입력을 받게 됩니다.

차이가 있다면 run1과 run2를 수행하는 순서겠네요. 아마도 먼저 부르는 쪽이 라이브러리 함수를 참조하는데 아직 메모리 상에 안 올라와 있으니 미세한 딜레이가 생기고, 나중 부르는 쪽은 그걸 활용하니까 딜레이가 적은 게 아닐까 추측합니다.

Stephen Kyoungwon Kim의 이미지

$ diff run1.c run2.c
6c6
< void run1()
---
> void run2()
17c17
<                 x=x+a;
---
>                 x+=a;
20c20
<         printf("Time : x=x+a\t%lf\n",(double)(end - start)/CLOCKS_PER_SEC);
---
>         printf("Time : x+=a\t%lf\n",(double)(end - start)/CLOCKS_PER_SEC);

그리고 컴파일을 해서 덤프된 김플 트리를 비교해 보면 이렇습니다.

$ for i in $(ls run*.c); do gcc -c -fdump-tree-optimized $i; done
$ diff -U4 ./run1.c.231t.optimized ./run2.c.231t.optimized 
--- ./run1.c.231t.optimized 2020-03-29 20:45:58.155611788 -0700
+++ ./run2.c.231t.optimized 2020-03-29 20:45:58.191612126 -0700
@@ -1,8 +1,8 @@
 
-;; Function run1 (run1, funcdef_no=0, decl_uid=2441, cgraph_uid=1, symbol_order=0)
+;; Function run2 (run2, funcdef_no=0, decl_uid=2441, cgraph_uid=1, symbol_order=0)
 
-run1 ()
+run2 ()
 {
   int i;
   int x;
   int a;
@@ -37,9 +37,9 @@
   end_15 = clock ();
   _1 = end_15 - start_12;
   _2 = (double) _1;
   _3 = _2 / 1.0e+6;
-  printf ("Time : x=x+a\t%lf\n", _3);
+  printf ("Time : x+=a\t%lf\n", _3);
   return;
 
 }
 

차이가 이름밖에 없죠.

다음 코드를 갖고 순서를 바꿔가며 수행해 보면 먼저 되는 쪽이 느린 것으로 보입니다.

// main.c
 
extern void run1();
extern void run2();
 
int main(int argc, char* argv[])                                                                                                                                                                                                                                                
{                                                                                                                                                                                                                                                                               
  if (argv && argv[1]) {                                                                                                                                                                                                                                                        
    run2();                                                                                                                                                                                                                                                                     
    run1();                                                                                                                                                                                                                                                                     
    return 0;                                                                                                                                                                                                                                                                   
  }                                                                                                                                                                                                                                                                             
 
  run1();                                                                                                                                                                                                                                                                       
  run2();                                                                                                                                                                                                                                                                       
  return 0;                                                                                                                                                                                                                                                                     
}

$ for i in $(seq 1 5); do echo "iteration #""$i"; ./main.x; done
iteration #1
Time : x=x+a    1.395064
Time : x+=a 1.356947
iteration #2
Time : x=x+a    1.386593
Time : x+=a 1.357442
iteration #3
Time : x=x+a    1.357421
Time : x+=a 1.357517
iteration #4
Time : x=x+a    1.405108
Time : x+=a 1.362055
iteration #5
Time : x=x+a    1.362099
Time : x+=a 1.362000
 
$ for i in $(seq 1 5); do echo "iteration #""$i"; ./main.x; done
iteration #1
Time : x=x+a    1.395064
Time : x+=a 1.356947
iteration #2
Time : x=x+a    1.386593
Time : x+=a 1.357442
iteration #3
Time : x=x+a    1.357421
Time : x+=a 1.357517
iteration #4
Time : x=x+a    1.405108
Time : x+=a 1.362055
iteration #5
Time : x=x+a    1.362099
Time : x+=a 1.362000

대체로 먼저 불리는 쪽이 빨리 끝나는군요.

Stephen Kyoungwon Kim의 이미지

먼저 불리는 쪽이 빨리가 아니라 늦게 끝납니다.

익명 사용자의 이미지

코드 분리 후, 따로 실행 했습니다만.. 그래도 x=x+a 방식이 빠른데요..

다른 버전의 gcc에서는 틀린 값이 나올까요?

1차 테스트 ( x=x+a 실행 후, x+=a 실행 )

root@ip-172-31-5-82:~# for i in {1..10}; do ./a;done
Time : x=x+a    2.693436
Time : x=x+a    2.687800
Time : x=x+a    2.669772
Time : x=x+a    2.656392
Time : x=x+a    2.660385
Time : x=x+a    2.670424
Time : x=x+a    2.667108
Time : x=x+a    2.704263
Time : x=x+a    2.683116
Time : x=x+a    2.659864
root@ip-172-31-5-82:~# for i in {1..10}; do ./b;done
Time : x+=a     2.768533
Time : x+=a     2.771669
Time : x+=a     2.760434
Time : x+=a     2.788213
Time : x+=a     2.736099
Time : x+=a     2.753531
Time : x+=a     2.752593
Time : x+=a     2.753506
Time : x+=a     2.742835
Time : x+=a     2.745188

2차 테스트( x+=a 실행 후, x=x+a 실행 )

root@ip-172-31-5-82:~# for i in {1..10}; do ./b;done
Time : x+=a     2.746433
Time : x+=a     2.755847
Time : x+=a     2.824680
Time : x+=a     2.824381
Time : x+=a     2.739385
Time : x+=a     2.731376
Time : x+=a     2.753780
Time : x+=a     2.768470
Time : x+=a     2.768796
Time : x+=a     2.771379
root@ip-172-31-5-82:~# for i in {1..10}; do ./a;done
Time : x=x+a    2.677279
Time : x=x+a    2.672089
Time : x=x+a    2.662083
Time : x=x+a    2.661391
Time : x=x+a    2.686135
Time : x=x+a    2.705060
Time : x=x+a    2.692804
Time : x=x+a    2.681449
Time : x=x+a    2.668290
Time : x=x+a    2.690885

gcc version

root@ip-172-31-5-82:~# gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.12' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12) 

익명 사용자의 이미지

흥미롭네요 위 글은에는 aws 리눅스 머신이고

아래는 바이메탈 서버 입니다.

동일 우분트 버전이구요.

하드웨어 스펙만 차이날뿐 나머지는 같은 상황에서

바이메탈 서버에서는 속도 차이가 없습니다.

컴파일러 차이 보다는 하드웨어 차이일까요?

AWS : Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz Single
바이메탈 : Intel(R) Xeon(R) CPU E5310 @ 1.60GHz Dual

바이메탈서버 테스트

root@monitoring:~# for i in {1..10}; do ./a;done
Time : x=x+a 4.265288
Time : x=x+a 4.266142
Time : x=x+a 4.265429
Time : x=x+a 4.265580
Time : x=x+a 4.265780
Time : x=x+a 4.265248
Time : x=x+a 4.265903
Time : x=x+a 4.265418
Time : x=x+a 4.282932
Time : x=x+a 4.265721
root@monitoring:~# for i in {1..10}; do ./b;done
Time : x+=a 4.265317
Time : x+=a 4.265702
Time : x+=a 4.265347
Time : x+=a 4.265338
Time : x+=a 4.266049
Time : x+=a 4.265362
Time : x+=a 4.265547
Time : x+=a 4.265337
Time : x+=a 4.265762
Time : x+=a 4.265639

gcc version

root@monitoring:~# gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.12' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12) 
Stephen Kyoungwon Kim@Google의 이미지

상식적으로 완전히 동일한 코드가 성능 차이가 난다면 다른 요인이겠죠. 완전히 동일한 코드인가 아닌가는 아래 몇 가지로 확인해 보시면 됩니다.
1. -fdump-tree-optimzed
2. -c로 .o를 얻어서 objdump -d

기타 등등..

제가 보기에 이 코드의 유일한 차이는 printf에 들어가는 출력 메시지밖에 없어 보입니다.

익명 사용자의 이미지

네 말씀하신대로 확인 해보면 printf 쪽만 틀릴뿐 완벽하게 동일합니다.

하드웨어에 따라 틀린 결과가 나온다는게 정말 재미있는 부분인네요

결국 x+=a 와 x=x+a 중에 누가 더 빠른지 단정짓기가 어렵네요

CPU만드시는 분들한테 물어 봐야 하는지... 참 어렵네요.

Stephen Kyoungwon Kim@Google의 이미지

그래도 그 두 가지에 의한 차이는 CPU 수준에서 보이지 않습니다. 그분들에게 주어지는 입력은 컴파일러가 번역하고 링커가 링킹을 한 익스큐터블이죠. 그게 같다면, 그건 더 이상 코드로 인한 차이는 아니라고 봐야죠.

익명 사용자의 이미지

++, +=, a = a + 1 의 연산 속도 차이는 c 언어 스펙에 따른 차이가 아닌, 컴파일러 구현상의 차이라고 봅니다. 그래서 별 의미가 없다고 생각합니다. gcc, clang 소스코드 보고 이러이러해서 이러한 결과가 나온다... 이런 얘기가 가능하겠지만, 5년, 10년 지나면 소스코드가 변하기 때문에 무의미해집니다. 그래서 위 질문/호기심은 컴퓨터과학자 또는 바로 지금 컴파일러를 개발/유지보수할 개발자/기여자가 아니라면 아무 의미 없다고 생각합니다.
호기심을 풀어봤자, gcc, clang 코드 최적화에 지금 기여할 것으로 예상하지도 않고요.

Stephen Kyoungwon Kim의 이미지

컴파일러 구현 상에서도 차이가 없습니다. 물론 학부생이 컴파일러를 만든다면 차이를 낼 수도 있겠습니다 (나쁜 의미로). 그런데 컴파일러 엔지니어가 컴파일러를 설계한다면 상식적으로 차이가 나지 않는다고 생각합니다.

유일하게 차이가 있을 수 있는 부분은 x++과 x += 1 혹은 x = x +1인데, 이건 컴파일러 구현 상의 차이가 아니라 undefined behavior를 포함해서 소스 코드 상의 의미가 다소 다르기 때문에 생기는 것이고, 소스 코드 상의 의미가 (sequence point로 인해) 같다면 차이가 생기지 않는 경우가 대부분일 겁니다.

별로 큰 의미가 있는 질문도, 퍼포먼스에 영향을 주지 않는다는 말씀에도--당연하죠, 차이가 없으니--동의합니다.

익명 사용자의 이미지

질문자님께서는 컴파일러 구현상의 차이로 인하여 빠를 수도 느릴 수도 있겠다... 이 정도로 이해하시면 되겠습니다.
옛날 90년대에는 gcc 로 생산된 바이너리가 인텔 컴파일러로 생산한 바이너리보다 대략 30% 정도 느렸습니다.
요즘은... 인터넷에 돌아다니는 벤치마크 자료를 보면 gcc, intel 컴파일러 생산 바이너리 속도가 비슷한거 같네요.

한줄 요약:

컴파일러 구현상의 차이로 인하여 빠를 수도 느릴 수도 있고 그 차이가 0.00000001 정도라 사실상(현실적으로) 무의미하다.. 이렇게 이해하시면 되겠습니다.

사족:

저는 90년대 공부한 사람인데.. 교수님인지 강사님인지 선배님인지 지금 기억으로 누가 얘기했는지 확실하지는 않지만,
확실하게 +=, ++ 연산이 더 빠르다고 그렇게 교육받았습니다. 옛날(90년대)에는 gcc 로 컴파일하면 속도가 솔라리스 cc 또는 인텔 cc 보다 엄청 느렸는데, 20년도 훌쩍 지난 지금 속도상 의미있는 차이가 없는거 같습니다. 벤치마크 자료를 보면 서로 앞서거니 뒷서거니 합니다. 삐까삐까 하다는 얘기죠. 학문적 관심이나 컴파일러를 개발하고자 하는게 아니라면 호기심 푸는 거는 여기까지만 하고 그 시간에 보다 생산적인 자료구조, 인공지능 등을 공부하는게 좋다고 생각합니다.

Stephen Kyoungwon Kim의 이미지

한 줄 요약에는 동의합니다.

90년대에도 그런 인식은 잘못 되었을 가능성이 적지 않다고 생각합니다. 솔라리스 스튜디오가 ICC나 gcc와 비교해서 좋은 부분도 나쁜 부분도 있습니다.

그리고 인공지능도 생산적입니다만 컴파일러 역시 생산적인 주제라고 생각합니다. 컴파일러 개발 안 해도 컴파일러 스킬이 도움 되는 곳은 많습니다. 제가 일하는 곳도 그 중 하나입니다.

Stephen Kyoungwon Kim의 이미지

수정이 안 되어 불편하네요.
"솔라리스 스튜디오가 ICC나 gcc와 비교해서 좋은 부분도 나쁜 부분도 있습니다."

이건 지워지지 않고 잘못 남기게 된 부분입니다. 하려던 말은, 어떤 주제는 컴파일러의 구현 문제라고 얘기할 수 있지만, 본문의 질문은 그런 범주로 보이지 않는다는 말씀이었습니다.

예컨대 h.264의 핫 루프를 보면서 벡터화 얘기를 할 땐 컴파일러 구현 얘기가 보다 의미있겠습니다만, 본문의 예는 구현에 따라 크게 다를 것 같지 않습니다. 25년 전이라고 해도 거의 마찬가지로 생각됩니다.

익명 사용자의 이미지

제가 90년대 학생 시절에 선배님(교수님, 강사님, 학과 선배님 통칭)들께서 그렇게 말씀하셨으니 당시 별다른 의문을 가지지 않았습니다. 당시 직접 벤치마크도 수행해본 바 없고요. 선배님들이 그렇게 말씀하시기까지 뭔가를 하셨겠죠. 지금이 2020년인데 지금 가치관으로 과거를 판단하는 건 무리가 있다고 봅니다.
그리고 제가 과거 90년대에 gcc 를 쓰게 된 계기가 솔라리스 cc 라이선스비 엄청 비쌌나봅니다. 동시에 가동이 5개 밖에 안 되었어요(라이선스를 5개 분량 밖에 준비를 안 했나보죠. ps 해보면 라이선스 체크기가 데몬으로 돌아가는 거 같았습니다.). 그래서 다른 사람 컴파일 끝날 때까지 기다려야 했었고, 기다리는데 시간을 날리기가 싫어서 gcc 를 사용하게 되었죠. gcc 로 컴파일하여 생산한 바이너리의 수행속도가 솔라리스 cc 생산한 바이너리 수행속도보다 느렸다는건 확실하게 기억합니다.
리눅스 좋아하시는 분들 리눅스/자유/오픈소스쪽 도구에 대해 낙관적/긍정적으로 보시는 분들 많으실텐데, 옛날 90년대 2000년대만 보더라도 벤치마크 자료보면 상업용 유닉스보다 성능이 떨어졌습니다. 지금 2020년이야 의미없는 얘기지만요.
그리고 모든 도구는 좋은 부분도 있고 나쁜 부분도 있습니다. 그게 자기한테 좋으냐 나쁘냐 그게 중요하죠.
그리고 학생 시절에는 한정된 시간에 어려운 컴파일러 기술보다는 깊이 파고들기보다는 보편적으로 많이 사용하며 필수적으로 알아야 되는 기술들(자료구조, 데이터베이스 이론, 인공지능 기본, 필수 알고리즘, c언어, 최소 1개 이상 동적언어)을 공부하는데 훨씬 도움이 되겠죠. 컴파일러 중요성을 모르는 사람이 어디있겠습니까.

Stephen Kyoungwon Kim@Google의 이미지

제 업계 경력의 거의 전체가 컴파일러 구현입니다. 말씀하신 세 가지 컴파일러 중 하나에는 제 코드가 꽤 들어가 있습니다.

당연히 컴파일러 최적화와 관련된 문제에 관해서는 어느 정도 인지하고 있습니다. 예컨대 솔라리스 스튜디오는 auto-vectorization 지원이 다른 컴파일러에 비해 좀 더딥니다. 그런 건 아주 간단한 모듈도 아니고 역사적인 이유도 있습니다.

그런데 x++, x+=1, x = x + 1과 같은 이슈들은 복잡도가 그다지 높을 일이 없습니다. 80년대라고 상황이 다를 문제가 아니라는 얘기죠.

인공지능은 제가 지금껏 일해온 분야에서는 사용되지 않는 기술입니다. 최근에 아마존 오퍼를 디클라인 했는데, 오퍼를 준 곳은 lab126이고 임베디드 시스템/커널 엔지니어를 여전히 많이 구합니다.

코드가 저수준에서 어떻게 컴파일되고 수행될 것인지를 이해할 필요가, 모든 자리에 다 있지야 않죠. 하지만 여전히 그런 자리가 있고 흥미로운 주제이므로 생산적이라고 말할 수 있다는 얘깁니다. 학부는 보통 4년 이상이고, 그 기간에 말씀하신 모든 것에 더해서 시스템 프로그래밍, OS, 컴파일러 등을 공부하기에 충분한 시간입니다.

예컨대 저희 팀은 버츄얼 머신을 하는데, 머신 러닝을 하는 사람과 커널을 하는 사람 중 누굴 뽑을까요? 물론 이 조직에서 컴펜세이션을 가장 크게 받는 부서는 인공지능 쪽이긴 합니다만 컴퓨터 엔지니어링 분야에는 다양한 재능들이 필요하다고 봅니다.

익명 사용자의 이미지

그리고 요새는 어플들 진짜 대충 막만들지만, 90년대만 보더라도 그러지 않았습니다. 90년대 후반 하드웨어 수준이 펜티엄, 펜티엄 프로(133MHz), 셀레론 이런 시대였고, 하드 디스크가.. 10MB, 20MB, 100MB, 500MB, 1GB 시절입니다. 메모리가 1MB 시절이었습니다. 컴에 16 MB 달면 학생들 사이에서 갑부라고 불리던 시절입니다.
그 시절에는 a++, a = a + 1 의 속도 차이가 중요했을수 있습니다. 일단 C 로 짠 후에, 속도를 조금이라도 빠르게 하려고 어셈블리로 최적화하던 시절입니다.

요새 사람들 진짜로 어플 대충 막 만듭니다.
요새 가치관으로 과거를 판단하는 건 무리가 있다는 걸 다시 한번 말씀드립니다.
90년대 대비하여 현대 하드웨어 속도가 1000배, 1만배 빨라졌습니다.
그럼에도 불구하고 현대의 소프트웨어의 성능 및 품질은 90년대 만큼도 못하죠.
90년대 시절은 CPU 가 기가급도 아닌, 100, 133, 266 Mhz 시절입니다. ㅋㅋㅋㅋ

익명 사용자의 이미지

지금 컴파일러를 가지고 90년대 상식을 평가하는 건 온당치 않은 일이겠지요.

gcc-1.27 정도를 들고 오면 어떻습니까? 1988년에 릴리즈된 버전입니다.

이런 오래된 컴파일러를 현대 시스템에서 빌드하여 사용할 수 있게 해 주신 분들께 일단 감사를 드리고요.

https://miyuki.github.io/2017/10/04/gcc-archaeology-1.html
https://kristerw.blogspot.com/2019/01/building-gcc-127.html

제공된 가이드라인대로 빌드하고, 간단히 예시 코드를 짜서 컴파일 해 봤습니다.

extern int x, n;
 
/* x <= x + 1 */
 
void x_plus_1_1(){ x = x + 1; }
void x_plus_1_2(){ x += 1; }
void x_plus_1_3(){ x++; }
void x_plus_1_4(){ ++x; }
 
/* x <= x + n */
 
void x_plus_n_1(){ x = x + n; }
void x_plus_n_2(){ x += n; }

컴파일 결과:

.text
	.align 4
.globl x_plus_1_1
x_plus_1_1: # void x_plus_1_1(){ x = x + 1; }
	pushl %ebp
	movl %esp,%ebp
	incl x
.L1:
	leave
	ret
	.align 4
.globl x_plus_1_2
x_plus_1_2: # void x_plus_1_2(){ x += 1; }
	pushl %ebp
	movl %esp,%ebp
	incl x
.L2:
	leave
	ret
	.align 4
.globl x_plus_1_3
x_plus_1_3: # void x_plus_1_3(){ x++; }
	pushl %ebp
	movl %esp,%ebp
	incl x
.L3:
	leave
	ret
	.align 4
.globl x_plus_1_4
x_plus_1_4: # void x_plus_1_4(){ ++x; }
	pushl %ebp
	movl %esp,%ebp
	incl x
.L4:
	leave
	ret
	.align 4
.globl x_plus_n_1
x_plus_n_1: # void x_plus_n_1(){ x = x + n; }
	pushl %ebp
	movl %esp,%ebp
	movl n,%eax
	addl %eax,x
.L5:
	leave
	ret
	.align 4
.globl x_plus_n_2
x_plus_n_2: # void x_plus_n_2(){ x += n; }
	pushl %ebp
	movl %esp,%ebp
	movl n,%eax
	addl %eax,x
.L6:
	leave
	ret

결과를 보시면 아시겠지만, x_plus_1_* 함수들과 x_plus_n_* 함수들은 어셈블리 수준에서 완전히 같습니다.

예시 코드가 너무 간단한 탓도 있겠지 싶습니다만, 1988년의 gcc에게도 이 정도는 가뿐했다는 뜻이지요.

익명 사용자의 이미지

여기에 영어로 똑같은 질문 있네요.
clang

https://www.quora.com/Why-is-n++-faster-than-n-n+1

그러면 선배님들이 공부한 시절인 7,80년대로 거슬러 올라가서 8비트, 16비트 컴퓨터 시절로 거슬러 올라가야죠.

사람들이 이 질문에 낚여서 허우적 거리고 장문 댓글 다실 때, 저는 낮잠 자고 알고리즘 구상했습니다.
이런 무의미한 주제에 낚이지 마시고 남친, 여친 만나서 쳐 노는게 유익하다고 봅니다.

익명 사용자의 이미지

동일 익명입니다. 밥먹고 위 영문글 좀 읽어봤는데, 그쪽도 사람들 엄청 낚였네요 ㅎㅎㅎ
CISC, RISC 에 별 얘기 다 나옵니다. 근데, 현대시대에 무슨 의미가 있을까요.
옛날 80년대 말, 90년대에도 XT, AT(286), 386, 486 이런게 있었던거 같은데.. 저는 그걸 다 써봤지요 ㅋ
XT 시절에.. 음.. CPU 가 아마 4MHz, 10MHz 이정도 였던거 같은데 ㅎㅎㅎ
현대 시대에는 ++ 이 더 빠르냐 a=a+1 이 더 빠르냐.. 아무 의미 없습니다.
교수님께서 그런 얘기하시면 그냥 그런가부다 하고 넘어가면 됩니다.
제가 90년대(펜티엄 보급되던 시절)에 교육받을 때, 제가 생각하기에 백만분의 1, 천만분의 1 이정도 차이가 날 것이라 아무 의미가 없다고 생각하고 그냥 그런가부다 했습니다. 그 정도로 민감한 분야는 컴퓨터 언어 및 컴파일러를 사용하지 말고 기계어로 직접 코딩해야 맞는거죠. 흠~~~~~~~~

Stephen Kyoungwon Kim@Google의 이미지

교수가 그런 얘기를 하면 "이 교수는 뭘 모르는구나" "교수도 뭘 모를 수도 있구나" 하고 생각하기 바라서 답을 길게 적었습니다.

저도 반복해서 하는 얘기지만 저 정도는 그냥 넘어가도 됩니다. 그건 역설적으로 생성되는 코드가 매우 높은 확률로 같을 것이기 때문이죠. 아직도 number of iterations가 높은 핫 루프의 퍼포먼스를 신경 써서 작성해야 하는 어플리케이션이 적진 않습니다. 컴파일러 개발도 대체로 그런 루프를 갖다 놓고, 그 루프 자체가 워낙 널리 쓰여서 사용자더러 바꾸라고 할 수는 없으니 왜 성능이 느린지 분석해서 어플리케이션 프로그래머가 할 일을 컴파일러 엔지니어가 대신 해주는 거죠.

반대로 말하면, 어플리케이션을 어떻게 쓰느냐에 따라 컴파일러에 따라 최적화 될 수도 안 될 수도 있어요. 이게 먼 과거의 얘기도 아니고 최소한 2017년 초반의 이야깁니다. 지금도 on going이라고 확신합니다.

익명 사용자의 이미지

의미없는 건 맞지만
이런 주제로 얘기하는게 재미있지 않나요?ㅎㅎ

익명 사용자의 이미지

과거 90년대 공부하면서 선배님(통칭)들 얘기 무비판적으로 수용하고,
저런 논의 무의미하다고 말한 익명사용자인데요... ㅋㅋ
말은 그렇게 해도 저도 낚여서 ㅋㅋ

Stephen Kyoungwon Kim@Google의 이미지

여전히 다를 바 없습니다. 핵심은 두세 가지 입니다. 첫째, 프론트엔드가 파싱 후 AST를 다르게 생성할 것인가? 둘째, AST가 다르다면, IR 옵티마이저가 그 차이를 보존할 것인가? 셋째, 보존한다면, 코드 생성기가 increment를 하는 instruction을 평범한 x = x + 1 패턴에 적용하지 않거나 못할 이유가 있는가?

셋다 no 여야 다른 코드가 나옵니다. 그래야 다른 성능이 컴파일러 탓으로 blame이 될 수 있고요.

1980년대에도 x = x + 1과 x += 1은 AST를 다르게 할 이유가 없습니다. IR 옵티마이저부터 동일한 인풋이 들어가는데 다른 코드가 나올 수 없죠.

Stephen Kyoungwon Kim@Google의 이미지

x += 1과 x = x +1은 파싱 직후 AST가 같을 테고--아마 100명 중 99명의 컴파일러 설계자는 그렇게 하겠죠--, 아래 두 블록은 서로 다를 수도 있죠.

int x;
scanf("%d", &x);
int y = x++ + 1;

int x;
scanf("%d", &x);
x = x + 1;
int y = x + 1;

같을 수도 있지만 의미가 다르기 때문에 모든 컴파일러에서 다 같다고는 할 수 없습니다. 이런 경우가 아니라면, 저 정도로 단순한 코드 파싱-최적화-생성이 90년대 컴파일러에서는 달랐을 거라고 보기 힘듭니다. 그게 이 코드를 올리신 의도 중 하나라고 생각하고요.

익명 사용자의 이미지

제가 위에 소개한...

Why is n++ faster than n=n+1?

https://www.quora.com/Why-is-n++-faster-than-n-n+1

읽어보시는게 좋을거 같네요. (전문적인) 댓글이 50개 정도 있는거 같네요.

익명 사용자의 이미지

실명 내걸고 올린 답변들이 좀 더 믿음을 줄 수 있는 건 사실이지요.

Quora는 즐겨 찾는 사이트 중 하나입니다만, 읽는 사람에게 판단할 능력이 없으면 소용이 없습니다.

그래서야 교수님, 강사님, 선배로부터 배운 내용을 무비판적으로 수용하는 것과 다를 게 무엇인가요.

====

일견 무의미해 보이는 이런 논의는, 비록 쟁점이 되는 그 코드 자체는 무의미해 보일지라도 나름 교육적인 의미가 있습니다.

C언어가 고급 언어 중에서는 저수준 취급을 받긴 하더라도 여전히 고급 언어이고, 이를 통해 실제 컴퓨터의 동작을 정밀하게 제어하는 일은 어느 정도 한계가 있다는 것입니다.

C언어 표준에서는 이를 abstract machine과 observable behavior의 개념으로 설명합니다. 약간 더 자세하게 설명하면, C언어는 어떤 abstract machine의 동작을 기술하는 것이고 실제 구현은 abstract machine의 observable behavior를 보존하기만 한다면 어느 정도 재량이 허용되어 있다는 것입니다.

x += a와 x = x + a, ++i와 i+=1은 (각각 독립된 statement이라면) 동일한 abstrace machine behavior를 나타냅니다. 그러니 구현 측면에서 이 코드들이 조금이라도 서로 다르게 동작할 이유가 없는 것이죠. 이론적으로는요.

현실적으로 보면, 컴파일러가 꼭 이런 패턴들을 동일하게 컴파일해 줄 수 있다는 보장은 물론 없습니다. 하지만 이런 종류의 trivial한 정수 산술은 컴파일러가 꽤 잘 다뤄 왔거든요. 1990년대 기준으로 따지더라도 이 정도조차 못 하는 컴파일러가 과연 얼마나 실용적인 물건이었을지는 생각해 볼 문제입니다.

====

PS. C언어가 아니고, int가 아니라면 조금 달리 생각해 볼 요소가 있을 수도 있습니다. 그게 바로 맨 위에서 언어와 타입을 물어 본 이유이지요. Quora의 아래 답변도 같은 점을 지적하고 있습니다:

https://www.quora.com/Why-is-n++-faster-than-n-n+1/answer/Mario-Galindo-Queralt

예컨대 예전에 STL container의 iterator타입 I에 대해서 ++I;와 I++; 중 어느 게 더 빠른지 묻는 질문이 올라온 적 있었지요. 일견 extra copy를 만들지 않는 ++I가 더 빠를 것 같지만, 사실 그런 코드들도 굉장히 쉽게 최적화됩니다.

====

PPS. 컴파일러를 치워 놓고 생각했을 때, 즉 예컨대 x86(_64)에서 INC 명령이 ADD (with 1)명령보다 더 빠른지에 대해서도 생각해 보면 재밌습니다.

1) 자주 쓰이는 Register에 대한 INC 명령은 1바이트로 인코딩됩니다. 반면 ADD 명령어는 어떻게 해도 피연산자를 지정하기 위한 바이트가 추가로 들어가야 할 것 같아 보이는군요. 루프 크기를 줄이는 게 중요한 상황이라면 충분히 가치가 있을 수 있겠죠.

2) 그렇다고 INC가 ADD보다 항상 좋은가, 하면 그것도 또 애매합니다. INC는 Partial flag writer이거든요. CPU 마이크로아키텍처가 지금보다 훨씬 간단했던 옛 시절에는 큰 문제가 안 되었겠지만, 요즘은 이게 Extra overhead를 만드는 모양입니다. Intel optimization manual을 살펴보면...

Quote:
19.2.2.9 Flags usage

Many instructions have an implicit data result that is captured in a flags register. These results can be consumed by a variety of instructions such as conditional moves (cmovs), branches and even a variety of logic/arithmetic operations (such as rcl). The most common instructions used in computing branch conditions are compare instructions (CMP). Branches dependent on the CMP instruction can execute in the next cycle. The same is true for branch instructions dependent on ADD or SUB instructions.

INC and DEC instructions require an additional uop to merge the flags as they are partial flag writers. As a result, a branch instruction depending on an INC or a DEC instruction incurs a 1 cycle penalty. Note that this penalty only applies to branches that are directly dependent on the INC or DEC instruction.

Assembly/Compiler Coding Rule 3. (M impact, M generality) Use CMP/ADD/SUB instructions to compute branch conditions instead of INC/DEC instructions whenever possible.

Quote:
20.2.6 Integer Execution Considerations

20.2.6.1 Flags usage

Many instructions have an implicit data result that is captured in a flags register. These results can be consumed by a variety of instructions such as conditional moves (cmovs), branches, and even a variety of logic/arithmetic operations (such as rcl). The most common instructions used in computing branch conditions are compare instructions (CMP). Branches dependent on the CMP instruction can execute in the next cycle. The same is true for branch instructions dependent on ADD or SUB instructions.

INC and DEC instructions require an additional uop to merge the flags as they are partial flag writers. As a result, an INC or a DEC instruction should be replaced by “ADD reg, 1” or “SUB reg, 1” to avoid a partial flag penalty.

현대 CPU에서 INC나 ADD나 어지간하면 1사이클만에 실행되는 간단한 명령어이기 때문에, 이런 extra penalty는 꽤 뼈아픕니다. 한 사이클만 늦어져도 latency가 100% 증가하는 거니까요.

어떻게 보면, 컴파일러가 이런 복잡한 문제를 프로그래머로부터 가리고 있었던 겁니다. 참 재미있는 토픽 아닙니까? 이런 걸 공부하고 알아 가는 것도 즐거운 일이지요.

익명 사용자의 이미지

잘 정리해 주셨네요:)

익명 사용자의 이미지

헤헤 저는 과거 무비판적으로 수용했고, 저런거 할 시간이 잠이나 더 자구,, 유뷰트 보면서 쳐노는데..
정말 대단하시네요. 잘 읽었습니다. 감사합니다.

Stephen Kyoungwon Kim@Google의 이미지

기계어를 고르는 단계는 컴파일 마지막의 코드 생성 단계입니다. 스튜디오 컴파일러의 경우 UBE나 cg 컴포넌트가 하는 일입니다. 그런데 만약 프론트 엔드 (-O0)나 IR Optimizer (그 나머지 모든 경우)가 코드 생성기에 동일한 입력을 준다면, 어떤 인스트럭션이 선택되든 소스 코드에 따른 차이는 없게 됩니다. 그래서 명령어 차이를 보는 게 이 문제의 핵심은 아니었던 것으로 보이는 거죠.

입력이 다를 수 있는 현실적인 시나리오는 프론트 엔드가 생성한 IR에서는 ++이 보이는데, 그게 IR Optimzer를 거치지 않고 코드 생성기로 바로 넘어가고, inc의 성능이 일반적으로 낫지 않으며, 따라서 코드 생성기가 x = x + 1을 inc로 매칭하지 않는 경우 뿐일 겁니다. 그런데 이 경우는 inc의 성능이 나쁘기 때문에 IR Optimzer는 inc를 IR 단계에서 x = x + 1로 번경한 후 다른 최적화를 적용할 거고요. 따라서 이 경우는 굳이 인스트럭션의 차이를 볼 수는 있지만, 그래도 릴리즈 되는 코드에 그렇게 나갈 가능성은 별로 없어 보이구요.

그리고 위에 어딘가 다른 분이 얘기한대로 많은 응용들은 이런 차이가 중요하지 않고 또 여전히 어떤 응용들은 이런 차이가 중요하죠. 그리고 이런 차이가 의미없는 응용이라고 하더라도 공짜로 성능을 0.3% 개선해 주겠다고 한다면 싫어할 사람이 없죠. 그래서 클라우드 시스템에 올라가는 리눅스 배포판을 제공하는 업체들은 소프트웨어 최적화에 여전히 신경을 쓰고 자기 나름의 툴 체인 팀이 있는 경우도 흔하고요.

Stephen Kyoungwon Kim@Google의 이미지

제가 선 스튜디오 컴파일러 만드는 일을 거의 3년 하고 gcc도 몇 달, go lang compiler까지 합하면 5년을 했습니다. 석사 2년을 컴파일러 랩(서울대/카이스트/포항공대 중 하나)에서 했습니다. 박사 연구에서 쓰이는 툴도 LLVM + SystemC 조합이고 10만 라인을 넘는 제법 큰 코드였습니다. 이 정도면 미국에서 컴파일러 전문가로 분류됩니다.

계속 얘기하지만 하드웨어에 increment를 위한 인스트럭션이 있다는 건 이 문제의 핵심이 아니에요. 어셈블리로 직접 코딩을 하는 상황이 아니라 소스 코드를 컴파일러가 보고 파싱하고 최적화 해서 코드 생성기 (컴파일러의 백엔드 마지막단)에 줍니다. 그때 코드 생성기에 같은 입력이 들어간다면, 동일한 코드가 생성됩니다. 그래서 성능 차이는 소스 코드 차이에 있지 않다는 얘길 하는 거고요.

Stephen Kyoungwon Kim@Google의 이미지

제가 그 50명 대부분보다 이 문제에서 더 전문가일 겁니다. 예컨대 하드웨어 아키텍쳐 하시는 분들은 그분들 나름의 expertise가 있지만 컴파일러가 똑같은 코드를 준다면 그분들이 그 똑같은 머신 코드의 소스가 소스 코드 레벨에서 어떻게 서로 달랐는지 파악해서 하드웨어 레벨에서 다르게 처리할 방법이 있나요? 이게 C였는지 C++이었는지 저수준에서 파악하는 게 가능은 할 텐데, 이것도 소프트웨어가 아닌 하드웨어가 해결하기는 가격 대비 불가능한 미션이죠. 명령어 차이는 이 문제에서 대부분의 경우에 큰 의미가 없습니다.

익명 사용자의 이미지

kldp 에 님글들을 보면 굉장히 전문적인 느낌이 납니다. 저도 님께서 50명 대부분보다 이 문제에서 더 전문가일 것이라고 생각합니다. 그런데... 사람들이 너무 전문적인 주제에 대해서 잘 몰라서(저도 그렇구;;) 그래서 답변을 못 다는걸거에요.

Stephen Kyoungwon Kim@Google의 이미지

말씀 감사합니다. 저는 다른 분야에선 전문가라고 생각하지 않지만, 컴파일러 분야에서 주로 일을 했고 이 문제는 컴파일러에서는 아주 쉬운 문제에 속하기 때문에 저 자신을 이 문제에 한해선 전문가라고 칭할 만하다고 생각합니다. 그런데 인터넷 사이트 하나 던져주고 전문적인 댓글을 읽어보라니 좀 황당해서 답을 달게 되었습니다.

힘들다의 이미지

string같은 객체를 저렇게 한다고했다면
속도차이가 납니다 아주 많이 나죠

Stephen Kyoungwon Kim@Google의 이미지

string의 경우에는 "아주 많이"를 어떻게 정의하느냐에 따라 다릅니다. 완전히 같지는 않습니다. 그렇지만 time complexity는 같습니다. s와 a의 길이를 각각 M, N이라고 하고 move semantics가 있는 컴파일러라면 이 차이는 매우 높은 확률로 O(1)이 됩니다. 그러니 c++11 이후의 표준을 사용하신다면 차이가 거의 안 난다고 생각해도 된다고 저는 봅니다. 물론 s += a;와 s = s+a; 가운데 아무 거나 써도 된다면, 전자가 약간 낫고 글자수도 하나 더 적기 때문에 전자를 안 쓸 이유는 없겠지만요.

일반적인 "객체"라면 객체의 종류에 따라 다릅니다. s += a 같은 류의 연산을 할 때, a를 수용하기 위해 버퍼를 확장할 때, 원래 데이터를 카피해야 하느냐 아니냐가 중요할 거 같습니다. string은 해야 하는 쪽으로 알고 있고, vector 같이 버퍼를 관리하는 객체라면 s와 a의 길이가 중요한데 대체로 s가 길면 안 해도 되고 확률상 s가 더 길겠죠. 이 경우는 s = s + a꼴의 연산이 O(s)만큼 느릴 것이고, 차이가 아주 많이 난다고 얘기해도 공정하겠죠.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.