-
[C++] Branchless 가 얼마나 잘 작동할까?쾌락없는 책임 (공부)/C++ 짜잘이 2023. 8. 23. 18:30반응형
개요
원래는 큰 생각 없었던 부분인데 유튜브 알고리즘 덕분에 생각해 볼 자료를 가지게 되었습니다.
이곳에서 Branchless에 대한 이야기를 하고 있었습니다. 저 같은 경우 학교 컴시구에서 if 구문이 명령어로 변경되었을 시 true, false 중 한곳을 맞다고 생각하고 가정한 뒤 계산을 하다가 그 가정이 틀리게 되면 이전 계산을 중지한 뒤 새 계산을 한다는 이론을 알고 있었습니다. 그래서 if문이 없으면 더 빠르지 않을까? 하고 막연하게만 생각해 왔습니다.
이번 영상을 보면서 제 생각이 마냥 옳다는 게 아니라는 걸 알아내서 이번 기회에 이에 대해 한번 적어보려고 합니다. 간단한 메모용으로요.
단순 수 비교에 있어서는 Branchless 보다 컴파일러가 더 믿을만하다
영상 초입에서 나오는 문제사항으로 두 개의 int 수를 두고 이중 더 작은 수를 반환하는 작업을 하는 것입니다. 이곳에 있어서 if 문을 통한 비교 후 반환을 하는가 아니면 if 없이 반환을 하는가에 대한 검증이 있었습니다.
// if문을 사용한, 일반적으로 생각되는 코드 int Min_Int1(int a, int b) { if(a < b) return a; else return b; }
// if문의 사용 없이, true, false를 활용한 연산 int Min_Int3(int a, int b) { return a * (a < b) + b * (b <= a); }
추가적으로 영상에서는 없는 삼항 연산자를 통한 함수를 추가해 테스트를 해 봤습니다.
int Min_Int2(int a, int b) { return (a < b) ? a : b; }
더보기전체 코드
#include <iostream> #include <chrono> using namespace std; int Min_Int1(int a, int b) { if(a < b) return a; else return b; } int Min_Int2(int a, int b) { return (a < b) ? a : b; } int Min_Int3(int a, int b) { return a * (a < b) + b * (b <= a); } constexpr int COUNT = INT32_MAX - 1; int main() { int a = 7; int b = 6; { cout << "1. Use if statement\n"; auto start = chrono::system_clock::now(); for(int i = 0; i < COUNT; i++) { Min_Int1(a, b); } auto end = chrono::system_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << duration.count() << "\n ============================\n"; } { cout << "2. Use ternary operator\n"; auto start = chrono::system_clock::now(); for(int i = 0; i < COUNT; i++) { Min_Int2(a, b); } auto end = chrono::system_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << duration.count() << "\n ============================\n"; } { cout << "3. Use branchless calculation\n"; auto start = chrono::system_clock::now(); for(int i = 0; i < COUNT; i++) { Min_Int3(a, b); } auto end = chrono::system_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << duration.count() << "\n ============================\n"; } }
결과는 if문의 승리, 컴파일러의 최적화를 기대할 수 있다!
그냥 cpu 이론을 간단하게 알고 있는 상태에서는 if문의 사용이 더 오래 걸릴 것으로 봤으나 제 환경에서는 if문이 더 빠르게 나올 수 있었습니다. 약 21억 번의 int 수 비교를 했을 때 branchless가 50% 더 느린 것을 확인할 수 있었습니다.
- 시간은 μs단위
a = 5, b = 6 a = 6, b = 6 a = 7, b = 6 if 2032362 2156047 2166109 ternary operation 2452248 2236292 2237679 branchless 3166223 3113922 3113913 위 경우들을 시행했을 때 전부 if문이 빠르게 나옴을 알 수 있었습니다. 3항 연산자의 경우 branchless보다는 빠르게 나오지만 if문의 시간을 단 한 번도 앞지르지 못했습니다.
이는 영상에서도 알 수 있듯 컴파일러가 좋은 어셈블리어를 매칭해 줘서 그런 것이고 branchless의 경우 그 혜택을 받지 못한 채 더 많은 어셈블리어를 만들어 내기 때문입니다.
영상에서 나온 다른 Branchless는 어떨까?
근데 이건 또 아닙니다. Branchless 가 쓸모 있다는 부분을 영상에서 소개를 해 줬는데요. 그 주제는 "문자열에서 알파벳을 전부 대문자로 변경하기"였습니다. 일단 영상에서는 Branchless로 개발한 곳이 더 나은 성능을 보여준다고 합니다. 최대 7~8배 정도 빠른 속도가 나온다고 하는데요.
저 같은 경우 GCC 환경에서 실행했을 때 1,000,000,000자리의 string을 Branch로 변경하는 게 더 빠르게 계산되었습니다. 영상과는 확실히 다른 이야기가 되네요.
더보기#include <iostream> #include <chrono> #include <string> using namespace std; string GenerateRandomString() { static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; string resultString; resultString.reserve(1000000000); for (int i = 0; i < 1000000000; ++i) { resultString += alphabet[rand() % (sizeof(alphabet) - 1)]; } return resultString; } void BranchConverter(string& s) { for(int i = 0; i < s.size(); i++) { if(s[i] >= 'a' && s[i] <= 'z') { s[i] -= 32; } } } void BranchlessConverter(string& s) { for(int i = 0; i < s.size(); i++) { s[i] -= 32 * (s[i] >= 'a' && s[i] <= 'z'); } } int main() { { cout << "1. Convert with Branch\n"; string testString = GenerateRandomString(); auto start = chrono::system_clock::now(); BranchConverter(testString); auto end = chrono::system_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << duration.count() << "\n ============================\n"; } { cout << "2. Convert with Branchless\n"; string testString = GenerateRandomString(); auto start = chrono::system_clock::now(); BranchlessConverter(testString); auto end = chrono::system_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << duration.count() << "\n ============================\n"; } }
컴파일러마다 차이가 있을까 해서 봤을 때 VS에서 실행에도 역시 Branchless가 더 늦은 속도를 보였습니다.
VS Code (with GCC) VS (C++14) VS (C++17) if 8038199 24729922 25337510 Branchless 8768503 29587214 29824873 일단 모든 경우에서 if문을 사용하는 게 더 빠르게 나오는 걸 볼 수 있었습니다. 아무래도 환경의 차이인지 영상과는 다른 결과가 나와서 당황스럽네요.
나만의 결론
일단 위 2가지 경우에서 컴파일러의 최적화에 기대는 게 더 좋은 결과를 가져온다는 걸 확인할 수 있었습니다. 또한 각 환경마다 속도가 달라질 수 있다는 걸 알게 되었으니 이를 잘 기억할 필요가 있겠습니다.
무조건적인 이론 숭배보다는 실제 나의 개발 환경에서 속도를 측정해 보고 사용해 보자는 생각이 더 강해지네요. 또 branchless 구문들의 경우 한 번에 보고 확 이해되지는 않고 이 수식을 이해할 필요가 있어 유지보수에 마냥 좋은 코드는 아닐 거란 생각이 듭니다.
마지막으로 위 영상에서 댓글을 보아하니 GPU 코드를 작성하는 데는 이런 Branchless 구문들을 많이 보게 된다는데 혹시나 이쪽 환경으로 넘어가게 된다면 잘 기억해 보도록 하겠습니다.
반응형'쾌락없는 책임 (공부) > C++ 짜잘이' 카테고리의 다른 글
[C++] 특정 클래스만 함수를 호출할 수 있게 하는 방법 - Attorney and Client Idiom, Passkey Pattern (0) 2023.10.22 [C++] vector 는 2배씩 늘어나는게 맞을까? (0) 2023.09.21 [C++] ptr[offset], offset[ptr] 은 같은 값이다? (0) 2023.03.16 [C++] std::move return은 정말 필요할까? (1) 2023.01.22 [C++] sort 함수에 함수 객체가 좋을까 함수가 좋을까? (0) 2022.09.02