[해결] GCC Inline Assembly의 효용성에 대해...
안녕하세요.
C/C++ 공부 시작한 지 2 개월 조금 덜 됐고, KLDP도 방금 막 가입한 초짜&신입입니다.
3일 전에 GCC Inline Assembly에 대해 알게 되서 어셈블리어 공부를 막 시작했는데요....
그것이 굳이 쓸 가치가 있는 지에 대해 궁금하게 되어서 이렇게 질문 올려봅니다.
일단 다음과 같이 std::string형 string을 std::wstring형 string으로 변환하는 함수를 asm과 C++ 두 가지로 작성해봤습니다.
/* Function Converts std::string to std::wstring. Written by asdefary. Only can be used to UTF-8 charset */ #include <string> using namespace std; wstring convsws(string str) { #ifdef __USE__ASM__ //ASM을 사용하기로 할 때 wchar_t tostr[str.length() + 1]; // char[] -> wchar_t[]은 언제나 길이가 줄어듦으로 반환할 wchar_t 문자열의 길이를 char 문자열의 길이와 동일하게 설정 __asm__ __volatile__( "wait;" "xorl %%eax, %%eax;" "xorl %%ebx, %%ebx;" "0:" "lodsb;" "testb %%al, %%al;" "je 0f;" "movb %%al, %%ah;" "movw %%ax, %%dx;" "shll $16, %%edx;" "movw %%ax, %%dx;" "1:" "shrb $6, %%dl;" "xorb $0x02, %%dl;" "testb %%dl, %%dl;" "jne 2f;" "shll $6, %%ebx;" "andb $0x3f, %%ah;" "orb %%ah, %%bl;" "jmp 0b;" "2:" "testl %%ebx, %%ebx;" "je 3f;" "pushl %%eax;" "movl %%ebx, %%eax;" "xorl %%ebx, %%ebx;" "stosl;" "popl %%eax;" "3:" "shrb $7, %%dh;" "testb %%dh, %%dh;" "jne 4f;" "xorb %%ah, %%ah;" "stosl;" "jmp 0b;" "4:" "shrl $16, %%edx;" "shrb $5, %%dl;" "xorb $0x06, %%dl;" "testb %%dl, %%dl;" "jne 5f;" "andb $0x1f, %%ah;" "orb %%ah, %%bl;" "jmp 0b;" "5:" "shrb $4, %%dh;" "xorb $0x0e, %%dh;" "testb %%dh, %%dh;" "jne 6f;" "andb $0x0f, %%ah;" "orb %%ah, %%bl;" "jmp 0b;" "6:" "movw %%ax, %%dx;" "shrb $3, %%dl;" "xorb $0x1e, %%dl;" "testb %%dl, %%dl;" "jne 7f;" "andb 0x07, %%ah;" "orb %%ah, %%bl;" "jmp 0b;" "7:" "shrb $2, %%dh;" "xorb $0x3e, %%dh;" "testb %%dh, %%dh;" "jne 8f;" "andb $0x03, %%ah;" "orb %%ah, %%bl;" "jmp 0b;" "8:" "shrb $1, %%al;" "xorb $0x7e, %%al;" "testb %%al, %%al;" "jne 0b;" "andb $0x01, %%ah;" "orb %%ah, %%bl;" "jmp 0b;" "0:" "testl %%ebx, %%ebx;" "je 0f;" "movl %%ebx, %%eax;" "stosl;" "0:" "xorl %%eax, %%eax;" "stosl;" : : "S" (str.c_str()), "D" (tostr) : "%eax", "%ebx", "%edx"); #else //ASM을 사용하지 않기로 할 때 wstring tostr; wchar_t b = 0; unsigned char c; for(unsigned int i=0;i<str.length();i++){ c = (unsigned char)str[i]; if((c >> 7) == 0) { if(b) tostr += b; tostr += c; b = 0; continue; } if(((c >> 6) ^ 0x02) == 0) { b <<= 6; b |= c & 0x3F; } if(((c >> 5) ^ 0x06) == 0) { if(b) { tostr += b; b = 0; } b |= c & 0x1F; } if(((c >> 4) ^ 0x0E) == 0) { if(b) { tostr += b; b = 0; } b |= c & 0x0F; } if(((c >> 3) ^ 0x1E) == 0) { if(b) { tostr += b; b = 0; } b |= c & 0x07; } if(((c >> 2) ^ 0x3E) == 0) { if(b) { tostr += b; b = 0; } b |= c & 0x03; } if(((c >> 1) ^ 0x7E) == 0) { if(b) { tostr += b; b = 0; } b |= c & 0x01; } } if(b) tostr += b; #endif return tostr; //wchar_t[]는 자동으로 std::wstring으로 변환됨 }
위 함수에서 asm으로 작성된 부분과 C++로 작성된 부분은 완벽하게 동일한 작업을 하게끔 작성한 것입니다.
하지만 직접 실험을 해본 결과.
"abcde가나다라마"의 20글자 문자열을 131072번(short형으로 2번 루프) 변환 하는데
asm 코드로는 50,000 clocks 정도,
C++ 코드로는 180,000 clocks 정도 걸립니다.
저는 개인적으로 asm으로 소스를 작성하는게 마음에 들어서 C++로 큰 틀을 잡고 (STL이 정말 편하더라구요... 근데 멀티쓰레딩 프로그램에 썼더니 으악..)
세부기능들을 C와 asm으로 작성을 하고 있는데 다른 분들의 의견을 들어보고 싶습니다.
질문은
1. GCC Inline Assembly의 효용성에 대한 의견
(쓰는건 괜찮지만 다른 작업 환경과의 호환성 때문에 비추천한다, 그냥 무조건 비추천한다, 써도 괜찮다 등)
정도 될 것 같고요... 추가 질문으로
pthread를 써서 멀티쓰레딩을 하는데 하나의 STL Container에 여러 쓰레드가 동시에 달려드니 Segment Fault가 뜨더군요...
그래도 STL의 S가 Standard인데... 자꾸 Segment Fault가 떠서 확돌아 갖고 어셈블리어로 컨테이너를 작성해서 쓰고 있습니다.
그래서 질문...
2. 멀티쓰레딩 환경에선 STL Container를 어떻게 관리해야 Segment Fault가 안 뜨나요?
노하우가 있으면 조금 알려주십사 합니다...
P.S. 포럼에 글 쓰는건 처음인데... 이렇게 쓰면 되는거 맞나요...
일단 제시하신 코드의 문제점부터
일단 제시하신 코드의 문제점부터 말씀드리겠습니다.
#1. tostr이 위치한 메모리가 다릅니다.
asm version의 경우, tostr의 memory가 stack위치 합니다.
STL version의 경우, tostr이라는 object는 비록 stack-memory area에 존재하지만, 내부에서 사용하는
버퍼의 위치는 heap영역에 할당되어있을 것입니다.
스택메모리로의 primitive-type 메모리 할당은 malloc()을 이용한 heap 메모리 할당보다 저렴할것입니다.
STL구현물에 따라, 메모리 풀을 사용할수도 있겠지만, 그렇지 않은 경우, brk()같은 system call 한번을 줄일수 있습니다.
#2. STL version의 tostr은 wstring 객체입니다.
wstring object를 생성할때마다,( 즉, 이 함수가 호출되어 wstring object가 stack에 할당될때마다 )
malloc등의 heap memory allocation을 사용하서 메모리를 할당하고 wstring의 생성자도 부를것입니다.
일단, 생성자의 오버헤드가 있습니다.
#3. wstring의 operator+() 는 아주 비싼 함수입니다.
STL version에서
tostr += b;를 매번 루프를 돌때마다 사용합니다.
wstring은 기본적으로 vector와 비슷한 class인데,
아무런 메모리 사이즈를 설정하지 않았을때는
기본 크기의 메모리 공간을 할당하여 사용합니다.( 얼마인지는 모르겠습니다만, 일단, 64 bytes 라고 하죠 )
그리고 byte input이 하나씩 들어올때마다 현재 가지고 있는 buffer에서 비어있는 공간이 얼마나 있는지
체크하고, 만약 부족한다면 심지어 realloc()도 합니다.
effective STL이었나? 어떤 책에서 STL에서 흔히 이용되는 string class 구현에 대한
몇가지 모델을 언급합니다.
작은 사이즈의 스트링과 큰 사이즈의 스트링 등을 구분하여 여러가지 복잡한 테크닉을 사용하기도 합니다.
lazy-copy를 위해 reference count를 사용하기도 하고요.
또한, 결국 operato+()도 함수이기에
각 character를 함수호출을 통해 (call-by-value) operator+()의 스택에 할당된 로컬변수로
값을 복사하고, 그값을 최종 internal-buffer에 복사합니다.
따라서, asm version에서는 각 문자마다 하나의 copy만 이루어지는데
STL version에서는 두번의 copy가 이루어집니다.
물론, operator+()는 inline하기 쉬운 함수라 실제로 하나의 copy가 이루어지게 할수도 있습니다만, 전적으로 컴파일러의 몫이며
보장되지 않습니다.
결론적으로 말하자면,
asm에서는 wchar_t 타입의
이미 알려진 fixed-length의 stack에 할당된 메모리에 루프를 돌고 있습니다.
하지만, STL의 wstring 입장에서는 매번 사용가능한 internal buffer size도 체크하고
realloc도 할수있으며, 생성자도 부르고, heap memory allocation을 위해 시스템콜도 사용할수도 있습니다.
즉, 메모리 operation부터 같은 상황이 아닌, 비교를 하고 계십니다.
=========================================
결국, 비교하시게 되는것은
C/C++ 코드와 inline assembly를 비교하는것이 아닌,
C/C++코드로부터 컴파일러가 생성한 assembly 코드와
직접 손으로 작성하신 assembly 코드를 비교하시는것 입니다.
전 개인적으로 inline assembly를 선호하지 않습니다.
일반적으로 컴파일러가 제 inline assembly보다 더 나은결과를 보여주는 경우가 많거든요.
만약, 정말 mission-critical하고, 성능이 정말 중요한 부분에서는
무조건 컴파일러를 믿지 않고, 직접짜는것이 아니라
컴파일러가 생성한 assembly 코드를 읽어보고, 개선할 부분이 있는지 검토해봅니다.
하지만, 요즘 컴파일러는 좋아서, 루프나 bitwise operation같은것이 단순한것들은 아무리 잘 짜봤자
컴파일러가 내는 최적화보다 좋게하기 힘듭니다.
inline을 많이 쓰는부분들은,
현재의 C/C++ 규격에서 처리하기 힘든, atomic operation이나
memory-fence 등을 다룰 때 씁니다.
대표적으로 CAS() operation을 생각해면 되겠죠.
또는, SIMD operation들...가령 MMX/SSE operation을 이용해서, 좀 더 tricky한
방법을 해야할 필요성이 있을때 씁니다.
x86의 mwait같은 instruction을 쓰고 싶을때도 씁니다.
이런경우들도, 컴파일러가 생성한 코드들을 보고, 비교해가면서 inline assembly를 짜는것이지,
무조건 assem 부터 잡는경우는 드뭅니다.(적어도 저의 경우는)
또한, 비교하실때도, 컴파일러의 최적화 옵션을 다르게 줘가면서 잘 비교해보세요.
물론, 디바이스 드라이버를 짜시는 분들의 입장을 다르겠지만, 적어도 user-level space에서의
application입장에서는 그런것 같습니다.
STL은 기본적으로 thread-safety에 대해서 언급하지 않고, 대부분의 구현물들은 thread-safety가 구현되어 있지 않습니다.
thread-safety는 아주 비싼 특징입니다.
가장 단순한 방법으로 mutex나 semaphore등의 sleep-lock을 쓰시게 되면
concurrency-level을 아주 떨어뜨릴 가능성이 높습니다.
과거처럼 single-core가 많았던 때에는 큰 차이가 없었을지 몰라도,
요즘같이 multi-core가 대부분인 지금은, critical section이 많아지면 대부분의 코어는
그냥 기다리고 있고, 하나의 코어만 열심히 일하는 상황도 펼쳐질수있습니다.( 물론, 이건 아주 극단적인 상황입니다만 )
그래서 critical section을 줄이거나 sleep-lock의 사용을 줄이기 위해,
쓰레드마다 별개의 객체를 갖게 한다던지 하는 방법도 있지만( 상황에 따라 )
이 역시, 메모리를 희생시키는 것입니다.
busy-waiting을 이용한,
lock-free algorithm 과 data-structure도 있습니다.
하지만, 이 경우도, CPU cycle을 소모하며, 상황에 따라 이 또한 독이 될수도 있습니다.
결론적으로, performance가 아주 중요한 자료구조의
thread-safety는 정말 상황에 맞게 최적화된 방법으로 구현되어야 합니다.
STL은 특정 환경이나 타겟을 두고, 작성된 framework이 아니라,
일반적으로 두루두루 쓰일수 있는 구현물을 목표로하기에( C++을 지원하는 모든 운영체제, 모든 플랫폼 )
thread-safety같은것을 넣기에 부담스러웠을것입니다.
STL을 thread-safe하게 쓰고 싶으면, 그냥 mutex같은 sleep-lock을 쓰세요.
사실, sleep-lock이 문제가 될수 있는 그런 부분들은 대용량 서버나 DB같은곳입니다.
대용량 서버도 sleep-lock을 효율적으로 잘 쓰면 문제가 안되는 경우가 많습니다.
C/C++ 배우신지 2개월밖에 안된 배우시는 신입이시라고 했으니,
저런거 신경쓰지말고 그냥 pthread_mutex_lock()같은거 이용해서 STL을 thread-safe하게 만드세요.
바보같지만, 상당히 잘 동작하며, mutex로 보호하는 critical section의 크기만 잘 줄여도
상당히 효과적으로 동작합니다.
제가 언급한 주제는 현업 C++ 프로그래머로, mission-critical한 부분에서
경력 3~4년은 쌓고, 생각해보세요.
신입이시라면, 저런 주제말고도, 성능향상을 위해 배워야 할 주제가 많습니다.
저런 주제는 program/architecture design도 어느정도 숙지하시고 배우셔도 늦지 않습니다.
memory management만 신경쓰셔도 성능향상 잘 됩니다.
P.S. 쓰다보니깐 너무 길어졌네요.
어셈은 일단 보류를 해둬야겠군요...
답변을 읽고 느낀게... '느린 이유가 있다'라고 할까요...
STL version이 asm version보다 느린게 '구조적' 문제 때문이지 '최적화' 문제 때문이 아니라면. 굳이 asm을 쓸 필요가 없을 것 같습니다.
말씀해주신 대로 메모리 관리나 기타 효율적으로 코드를 작성할 수 있는 방법을 공부해봐야겠습니다.
제가 애초에 asm을 건드리게 된게 STL + 멀티쓰레딩 문제 때문이었는데, 다른 해결 방법이 있다면 더더욱 asm을 잡을 필요는 없을 것 같네요.
mutex라.... 공부할게 아주 아주 많이 남았군요... 쩝.
나의 모든 생명은 단 한 명의 소녀를 위하여.
STL + 멀티쓰레딩 때문에 asm을 건드리신거라면
STL + 멀티쓰레딩 때문에 asm을 건드리신거라면 뭔가 포인트를 잘못 잡고 계시지 않은가 싶습니다.
멀티쓰레딩에서 겪으신 문제가 공유 자원에 대한 동기화 관련 문제가 아닌가 싶은데, 그렇다면 이는 asm으로 짠다고해도 비슷한 문제를 다시 겪게 되실 것으로 보입니다. 중요한건 '공유 자원에 대한 접근'이지 STL이 아니니까요.
제가 포인트를 잘못 잡은게 맞습니다...
일단 젠투 설치 하느냐 답변이 늦어서 죄송합니다...
말씀하신 대로 asm로도 똑같은 문제가 발생할 여지는 충분히 문제가 발생할 여지가 충분히 있으므로 제가 포인트를 잘못 잡은게 맞긴 합니다.
하지만 제가 생각한건 'asm을 사용하면 문제가 해결될 것이다'가 아니라, 'asm으로 문제를 해결할 방법을 만들어보자'였습니다.
'공유 자원에 대한 접근'을 쓰레드간 충돌이 없이 수행해 낼 수 있는 방법을 만드는것이 C++을 사용하는 것 보다 asm을 사용하는 것이 더 효율성이 높다고 판단하여서 asm으로 작업을 시도해봤던 것입니다.
나의 모든 생명은 단 한 명의 소녀를 위하여.
저는 개인적으로 asm으로 소스를 작성하는게 마음에
C++도, 자신이 뭘 사용하고있는지 정확히 안다면
C만큼 충분히 빠르게 만들수 있습니다.
다음은 제가 개인적으로 재밌게 읽은 책입니다:
Efficient C++: Performance Programming Techniques
책도 별로 두껍지 않고, 내용설명도 쉽게 잘 되어있어, 영어서적을 읽는데 익숙하시면 손쉽게 읽을수 있습니다.
저 책의 내용은 주로, C와 비교를 하면서
어떻게 C++코드를 짜면 느린것이고, 왜 느린것인지,
그렇다면 어떻게 짜야 C처럼 빠르게 나오는지 등에 대하여 설명을 중점적으로 다룹니다.
(torrent 뒤지면, e-book pdf로 돌아다니니, 보시고 맘에 들면 구매를 해보세요)
예를들어, c++의 virtual function에 대해 얘기할때,
virtual function의 성능과 관련한 문제는 vtable에 의한 오버헤드가 아니라고 설명합니다.
그것보다, virtual function을 쓸때의 단점은, 런타임에 동적으로 바인딩이 이루지기 때문에, 컴파일러 입장에서 최적화시키기가 어렵다는것입니다.
(가령 virtual function을 inline할수는 없죠.)
사실상, virtual function을 얻을때 얻는 코드상의 이점이 아주 크며,
더군다나 inline이 항상 성능향상 시켜주는것도 아니고,
요즘은 CPU들의 branch prediction도 너무 잘 발달하여
점점 virtual function같은 동적 함수 바인딩도 큰 성능하락 없이 사용할수 있습니다만..
제 개인적인 의견은
C가 C++보다 항상 빠른건 아닙니다.
C++도 잘 짜다보면, 둘다 비슷합니다.
단지, C++이 문제가 아니라, 정말 object-oriented 스타일로 코드를 작성하다보면
C에서는 존재하지 않았을, 불필요한 메모리카피나 함수호출을 야기 시키는 경우가 있어서( 대신, 코드는 사람이 읽기 좋은, OO스러운 코드가 되지만 )
성능에 차이가 나는 경우들도 있는것 입니다. 하지만, 이런부분들도 문제가 안되는경우도 많습니다.
위해 언급한 점들을 잘 인지하시고 조심히 코딩을 하면, 굳이 C++로 큰 틀을 잡고, 나머지를 C로 채우지 않아도 됩니다.
즉, 그냥 C++로만 짜도, 설계 잘 하고, 잘 짜면, 성능 잘 나옵니다.
처음 제시하셨던 STL 코드의 문제도
본인이 이용하는 STL이 어떻게 구현되어있는가를 모르고 사용하셔서 나온 문제 일뿐입니다. C++의 문제가 아닙니다.
좋은 답변 감사드립니다.
젠투 설치 하느냐 답변이 늦어서 죄송합니다...
말씀하신 대로 제가 C보다 C++이 상대적으로 느리다는 편견을 갖고 있었던 것 같습니다.
추천해주신 책 잘 읽고 편견에서 벗어나서 좋은 코드를 짤 수 있도록 노력하겠습니다.
나의 모든 생명은 단 한 명의 소녀를 위하여.
댓글 달기