[C++] as-if rule - 소스에 적힌 순서대로 실행되지 않는 이유

as-if rule 이란, 프로그램의 실행 결과가 변하지 않는다면 소스에 적혀있는 것이 아닌 다른 방법으로 실행하는 것을 허용하는 규칙을 말한다. 이러한 규칙을 as-if rule이라는 이름으로 부르는 것은 C++에서만 찾을 수 있지만, C를 비롯한 다른 언어에서도 이러한 부류의 최적화를 허용한다. as-if rule을 허용하는 이유는 크게 두 가지로 보인다. 우선 컴파일러의 최적화를 방해하지 않기 위함이다. 만약, 표준 문서에서 적용할 수 있는 최적화 방법을 화이트 리스트로 관리한다면, 새로운 최적화 기법이 나왔을 때, 컴파일러가 이를 적용하기 위해서는 표준을 수정해야 한다. 하지만 표준에서 허용하지 않는 동작만 기술해두면 컴파일러 구현체에서 새로운 최적화를 구현하는데 조금 더 자유로울 수 있다. 두 번째 이유는 C++이 가정하는 abstract machine이 엄밀하게 정의되지 않았기 때문이다. 일반적으로 C++의 실행 타깃은 native processor다. 이 프로세서는 제조사에 따라서, 혹은 칩 아키텍처에 따라서 다른 특성을 가질 수 있는데, C++ 표준은 이때 실행할 프로세서의 종류나 특성을 한정 짓지 않는다. 두 번째 이유는 C++이 가정하는 abstract machine이 실행 타깃에 의존하는 형태로 정의되었기 때문이다. 이를 parameterized nondeterministic abstract machine이라고 한다. 구현체는 이 parameter를 정해 실제 abstract machine을 완성한다. C++이 abstract machine을 이렇게 복잡한 방식으로 정의하는 이유는 C++의 실행 타깃이 가상 머신이 아닌 native processor이기 때문이다. 이 프로세서는 아키텍처에 따라서 다른 특성을 가질 수 있다. 다양한 아키텍처를 지원하기 위해 C++ 표준은 실행할 프로세서의 특성을 한정 짓지 않는다. CPU 별로 메모리 오더, 레지스터 크기, 레지스터 개수 등이 전부 다르므로 가능한 최적화도 전부 다르다. 따라서 표준에

[C++] Visual C++의 volatile

지난번 글 에서 말했듯이 C++ 11 이전에는 메모리 접근의 순서를 보장할 수 있는 표준 방법이 없었다. 따라서 플랫폼에 따라 다른 코드를 사용해야 했다. x86은 mfence , ARM이라면 DMB 인스트럭션을 인라인 어셈블리 를 사용하여 집어넣거나, gcc의 __sync_synchronize 나 Visual C++의 MemoryBarrier 매크로를 사용하는 방법이 일반적이다. 여기에 Visual C++은 volatile 에 추가적인 제약을 거는 방법으로 메모리 접근 순서를 보장하는 방법을 마련하였다. Visual C++의 컴파일 옵션에서는 volatile 의 동작을 2가지 중 하나로 선택할 수 있다. /volatile:iso 로 컴파일하면, iso 표준대로 메모리 접근 순서와 상관없이 as-if rule 에 의해 코드가 최적화되어 사라지는 것만을 방지한다. 하지만 /volatile:ms 로 컴파일하면 iso 표준에서 규정하는 제약에 추가적으로 volatile object에 접근하는 것이 load-acquire, store-release semantic 을 따른다. 즉, 간략히 말하면 store는 뒤에 있는 load가 실행된 다음에 실행될 수 있지만, 다른 메모리 접근의 순서는 순서대로 실행되는 게 보장된다. 컴파일 때 아무 옵션을 안 주면 x86에서는 /volatile:ms 가 기본값이다. 사실 x86 CPU는 load-acquire, store-release semantic을 따르기 때문에 volatile object에 접근하는 코드는 컴파일 타임에 순서를 바꾸지 않겠다는 것이다. ARM에서는 /volatile:iso 가 기본값이다. 따라서 /volatile:ms 를 이용하여 컴파일하면 DMB 인스트럭션을 이용하여 CPU가 순서를 바꿔 실행하는 것을 막기 때문에 성능이 떨어진다. 하지만 x86에서 테스트 된 코드를 그대로 사용할 수 있다.

[C++] memory barrier - 메모리 접근의 순서 보장하기

지난번 글 에서 말했듯이 C++의 volatile object에 대해서 메모리 접근 순서를 보장하지 않지만, 메모리 접근 순서를 보장한다고 잘못 아는 사람이 많이 있다. 이는 지난번에 글에서 말했듯이 Java나 C#으로 멀티 스레드 프로그래밍을 배운 사람들이 잘못 알기 때문이기도 하지만 C와 C++로 멀티 스레드 프로그래밍을 배운 사람들도 잘못 아는 경우가 많다. 그런 경우는 보통 아래와 같은 착각을 하기 때문이다. Access to volatile object s are evaluated strictly according to the rules of the abstract machine. - C++14 intro.execution 1.9.8.1 C++ 표준에 적혀있는 위의 문장에 따르면 volatile object에 접근이 엄격하게 실행된다고 적혀있다. 이 문장을 잘못 이해해서 엄격한 순서로 실행된다고 받아들이는 사람들이 있다. 하지만 이는 그저 volatile object에 접근하는 코드는 as-if rule 에 의해 최적화되지 못한다는 것으로 메모리에 접근해야 할 코드를 최적화해서 없애거나, 레지스터 등을 이용해서 최적화하지 못한다는 것이다. 그보다 중요한 것은 abstract machine의 규칙을 따른다는 것이다. 여기서 말하는 abstract machine은 다른 구현체에서 같은 동작을 보장하기 위해 C++ 표준이 기술한 가상의 기계를 의미한다. 근데 이 abstract machine은 의존성이 없는 다른 메모리 영역에 접근하는 것에 대해 순서를 보장하지 않는다. 이는 컴파일러가 as-if rule에 따라 최적화할 여지를 남겨두기 위해 서기도 하지만, 실제로 CPU가 실행 시간에 메모리 접근을 재배치 할 수 있기 때문이다. 그래서 실제로 어셈블리의 순서대로 실행될지 알 수 없다. 물론 모든 인스트럭션이 재배치되는 것은 아니고 CPU마다 자신이 재배치하여 실행할 수 있는 조합이 있다. 예를 들어 가장 많이 사용되는 X86의 경우 메모리에

C++의 volatile은 동시성과 상관 없다

volatile 은 C++에서 가장 오해받고 있는 키워드 중 하나일 것이다. 오해받는 이유는 크게 2개라고 생각한다. 첫 번째로 C++의 volatile 이 Java나 C# 등 다른 언어에서 말하는 volatile 과 다르기 때문이고, 두 번째로 C++의 volatile 은 CPU와 컴파일러가 어떻게 동작하는지 알지 않으면 이해하기 어려운 비직관적인 기능이기 때문이라고 생각한다. Java나 C# 등 다른 언어에서 volatile 은 다른 스레드에서 visibility를 보장해주기 위해 사용된다. 따라서 변수의 접근이 리오더링 되는 것을 막고, 언제나 최신 값을 유지하는 것을 보장해준다. 특히나 멀티 스레드 프로그래밍을 한다면 누구나 한 번쯤 읽어본다는 고전 명작 The Art of Multiprocessor Programming 에서 그 예제 코드가 Java로 돼 있기 때문에 C++에서도 volatile 키워드가 같은 의미를 가진다고 생각하는 사람이 많다. 하지만 C++에서 volatile 은 멀티 스레드와 아무런 관련이 없다. 이는 과거 C 표준에 스레드와 관련된 내용이 없던 시절에 추가된 이후로 지금까지 스펙이 변경되지 않았기 때문이다. 그래서 C++의 volatile 은 싱글 스레드 코드에서 변수의 접근이 최적화되어 사라지는 것을 막을 뿐이다. 예를 들어 int a 가 있을 때 a += 1 을 반복하는 코드가 3회 반복될 때, 이 중간에 a 에 접근하는 코드가 없으면, 이 코드는 a += 3 으로 최적화될 수 있다. 만약 중간에 a 에 접근하는 코드가 있어도, 매번 a 를 메모리에서 접근하지 않고, a 를 레지스터에 집어넣어 최적화할 수 있다. 하지만 a 의 타입이 volatile int 였다면, 이와 같은 최적화는 할 수 없고 a 를 메모리에서 읽어 1을 더하여 메모리에 쓰는 코드가 3번 들어가야 한다. 하지만 리오더링 되는 것을 막지 않는다. 따라서 의존성이 없는 두 변수가 volatile로 선언돼 있을 때 이 두 변수 사이에 읽기/쓰기는

[C++] C++ Core Guidelines - modern C++을 위한 안내서

C++은 사용하기 어려운 언어이다. C++ 이후에 나온 언어들에 비해 사용자에게 너무 많은 자유도를 주기 때문에 안전하게 사용하기 어렵다. 그래서 구글 , 웹킷 , LLVM 등 많은 스타일 가이드들이 단순히 문법적인 포맷을 어떻게 작성할지 디자인에 대한 부분도 같이 제안한다. 오늘 소개할 C++ Core Guidelines 도 이런 스타일 가이드 문서 중 하나다. modern C++, 즉, C++ 11 이후 C++을 어떻게 사용할지에 초점을 맞힌 가이드 문서로, C++의 창시자인 Bjarne Stroustrup이 주축이 되어 작성되었다. C++ Core Guidelines는 다른 문서보다 C++ 자체를 어떻게 하면 더 안전하게 쓸 수 있을지에 초점을 두기 때문에 문법적인 포맷에 대한 조언은 거의 하지 않는다. 그보다는 어떻게 하면 로직의 문제를 최대한 빠르게, 가능하면 컴파일 타임에 잡을 수 있는지에 대한 디자인 조언을 많이 한다. 개인적으로는 지금까지 읽었던 modern C++ 코딩에 관한 문서 중 가장 실용적인 문서라고 생각한다. C++ Core Guidelines를 읽다 보면 GSL이라는 단어가 자주 나온다. C++ Core Guidelines는 반복되는 몇 가지 사용 패턴을 위해서 몇몇 라이브러리를 사용할 것을 권하고 있다. 이를 Guideline Support Library, 줄여서 GSL이라고 부른다. 정의상으로는 가이드라인에서 제안하는 GSL의 스펙을 만족하는 라이브러리는 모두 GSL이라고 부를 수 있다. 하지만 현존하는 구현체는 Microsoft에서 만든 GSL 뿐이기 때문에 사실상 Microsoft의 GSL과 같은 의미라고 생각해도 된다. 앞으로 시간 나는 대로 C++ Core Guidelines와 GSL의 내용 중에서 좋아하는 항목 몇 개를 소개할 생각이다. 하지만 그와는 별개로 앞으로 계속해서 C++ 프로그래머를 할 생각이라면 한 번쯤은 읽어보기를 추천한다.

crash-only software - high availability server 만들기

지난번 글 에서 high availability 서버를 만들기 어려운 이유를 설명했었다. 그럼에도 high availability 서버를 만드는 것은 중요하기 때문에 availability를 높이기 위한 여러 가지 방법들이 존재한다. 이번에 설명할 crash-only software 도 그중 하나다. Crash-only software의 기본 철학은 해결할 수 없는 문제가 발생했을 때, 다른 시도를 하지 않고 바로 종료시키는 것이 오히려 availability를 올린다는 것이다. 이렇게 하면 서버가 떠 있는지만 검사하면 되기 때문에 서버에 문제가 생겼는지 바로 알 수 있고, 문제가 생기면 바로 crash로 끝내기 때문에 빠르게 종료할 수 있다. 다만 crash-only software는 언제든지 죽을 수 있기 때문에 persistence layer가 프로그램 외부로 빠져야 한다. 즉, 로직 레이어와 데이터 레이어가 구분되는 multitier architecture 가 된다. 자연스럽게 로직 레이어는 스테이트를 가지지 않게 되기 때문에 컴포넌트별로 독립시키기 쉽기 때문에 자연스럽게 마이크로 서비스 로 만들게 되고, 서비스 하나의 크기가 줄어들기 때문에 재시작에 걸리는 시간도 줄어들고, 자연스럽게 availability가 증가하게 된다. 또한, crash-only-software에서 사용하는 공유 자원은 사용권을 소유한다는 개념이 아니라 사용권을 빌린다는 개념으로 접근해야 한다. 사용권을 빌렸기 때문에 명시적으로 사용권을 돌려주지 않았더라도 일정 시간이 지나면 사용권을 잃고 다른 컴포넌트에서 사용할 수 있어야 한다.

high availability 서버를 만들기 어려운 이유

서비스에서 availability를 보장해주는 것은 매우 중요하다. three-nine(99.9%)의 availability를 보장하려면 일 년에 아홉 시간 이하의 다운타임만 있어야 하고, four-nine(99.99%)을 보장하려면 약 한 시간 이하, five-nine(99.999%)을 보장하려면 일 년에 다운 타임이 오 분 이하여야 한다. 보통 서버 장애는 서버가 가장 바쁠 때 발생하기 때문에 가능하면 다운 타임을 줄이는 것이 중요하지만, 현실적으로 이는 쉽지 않다. 상용 서비스 중에서도 three-nine 이상 보장하는 서비스를 찾기 힘들고, 어느 정도 이름 있는 서비스들은 돼야 four-nine, 정말 안정화가 잘 돼 있는 서비스들만이 five-nine 이상의 availability를 보장한다. 이는 high availability 서버를 만드는 것이 근본적으로 어려운 일이기 때문이다. 다운 타임이 적은 서버를 만들기 위해서는, 일단 버그가 없는 것은 기본이어야 한다. 하지만 서버 다운의 이유가 버그만 있는 게 아니다. 디스크나 네트워크 등 하드웨어 문제로 예상치 못하게 서버를 사용할 수 없게 되는 경우도 있다. 따라서 high availability 서버를 만들기 위해서는 문제가 생겼을 때 빠르게 재시작하는 것이 가장 중요한데 이를 위해서는 서버에 문제가 발생했을 때 이를 감시하고 있던 모니터가 빠르게 감지하여 서버에 문제가 발생하면 서버를 안전하게 죽이고, 새 서버를 띄워야 한다. 보통 서버에서 일정 시간 간격으로 메시지를 보내고 메시지가 오지 않으면 죽은 것으로 판단하는 heartbeat 방식 을 많이 사용한다. 이때 heartbeat 자체는 보통 몇 초에 한 번 보내지만, 실제 서버가 죽지는 않았지만, 네트워크나 다른 문제로 메시지가 오지 않았을 것을 대비하여 몇 번의 메시지가 도착하지 않았을 때 서버가 죽었다고 판단한다. 즉, 실제로 서버에 문제가 발생해도 그를 감지하는 데만 적게는 십수초 많게는 몇십초의 시간이 걸린다. 그다음

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API