2017의 게시물 표시

Raft - electionTimeout

broadcastTime ≪ electionTimeout ≪ MTBF Raft가 정상적으로 동작하기 위해서는 반드시 위의 조건을 만족해야 한다. electionTimeout은 leader election에서 설명한 random 한 timeout의 최대치를 의미한다. 사실 broadcastTime, electionTimeout, MTBF 중에서 사용자가 설정할 수 있는 것은 electionTimeout 뿐이다. 따라서 위의 조건을 만족시킨다는 것은 적절한 electionTimeout을 선택한다는 것이다. MTBF 는 평균 무고장 시간(Mean time between failures)의 약자로, 한 서버가 시작한 뒤 죽기 전까지의 평균 시간을 의미한다. 보통 MTBF가 길면 availability가 높지만, availability와 일치하지는 않는다. MTBF가 길더라도 MTTR (Mean time to repair)가 길면 availability은 떨어질 수 있다. MTBF는 시스템의 고유한 속성이다. MTBF는 보통 노드가 하는 일의 종류와 개발자의 숙련도, 얼마나 비싼 하드웨어를 사용하는지에 따라 결정된다. Raft paper 는 electionTimeout을 MTBF보다 작게 할 것을 권장한다. 실질적으로 MTBF는 최소 몇 주에서 몇 개월 정도 되기 때문에 electionTimeout을 이보다 더 크게 설정하는 것은 쉽지 않다. 이는 그냥 electionTimeout을 너무 크게 설정하지 말라는 정도로 받아들여도 된다. 리더가 죽은 뒤 electionTimeout 동안 client의 요청을 전혀 처리 못 하니 네트워크 전체의 availability를 올리기 위해서 가능하면 작은 electionTimeout을 설정해야 한다. 하지만 electionTimeout을 너무 작게 설정하면 안 된다. electionTimeout은 아무리 작아도 broadcastTime보다 커야 한다. broadcastTime이란, 한 노드에서 네트워크 안의 다른 모든

Raft - consistency

Raft는 모든 결정을 leader가 맡아서 한다. 따라서 term이 변경되기 전에는 leader의 결정을 따르면 된다. 문제는 leader에 문제가 생기거나 네트워크 파티션으로 인해 leader가 변경되고 다음 term으로 진행된 경우다. Consistency를 위해 가장 이상적인 것은 모든 노드가 하나의 leader만 따르도록 하는 것이다. 하지만 이는 사실상 불가능하다. 이게 가능하면 애초에 합의에 도달한 것이다. 그래서 Raft에서는 특정 시간에 2개 이상의 리더가 존재할 수 있다. 단, state를 변경시킬 수 있는 리더는 1개 밖에 있을 수 없다. 이 두 말은 별 차이 없는 것 같지만, 이 차이가 분산 환경에서 구현 가능한 시스템이 되도록 만들어준다. Raft에서는 leader에 커밋 된 로그만이 state를 변경시킨다. Leader가 커밋하기 위해서는 네트워크에 참여하는 노드 과반의 동의가 필요하다. 새 leader가 선출되면 과거의 leader는 절반 이상의 지지를 받지 못한다. 모든 요청에 요청하는 노드의 term이 담겨있고, 요청받은 쪽은 자신의 term보다 작은 term인 노드가 보낸 요청은 모두 거절한다. 새 leader가 선출됐다는 것은 이미 절반 이상의 노드가 다음 term으로 넘어갔다는 것이고 과거의 leader를 지지하는 노드는 절반이 되지 않기 때문에 과거 leader는 더 이상 상태를 변경시킬 수 없다. 따라서 같은 시간에 두 개의 노드가 상태를 변경시키는 것은 불가능하다. 물론 leader가 아닌 노드들이 가지고 있는 상태는 consistent 하지 않다. 새 RequestVote를 받기 전에 과거의 leader가 보낸 AppendEntries 메시지를 받고 자신의 상태를 변경시킬 수 있기 때문이다. 하지만 네트워크의 상태는 리더에 커밋 된 로그를 기준으로 만들어지기 때문에 각 노드의 inconsitecy는 클라이언트가 보는 네트워크 상태에 영향을 주지 않는다. 그렇다면 leader에 커밋 된 로그를 가지지 않은

Raft - log replication

Raft가 가장 중요하게 생각하는 요소는 이해할 수 있는 알고리즘을 만드는 것이다. 이해할 수 있고 구현하기 쉬운 알고리즘이 Raft의 가장 중요한 요소이기 때문에 Raft는 로그를 누적시킬 수는 있지만 지울 수는 없는 append only 정책을 사용한다. Append only 정책을 사용하기 때문에 Raft의 state를 바꾸는 명령은 AppendEntries 밖에 존재하지 않는다. 사실 Raft가 정의하는 필수 RPC( Remote procedure call )은 지난번 글에서 설명한 RequestVote와 이번에 설명할 AppendEntries 뿐이다. leader가 보내는 heartbeat은 빈 entry를 추가하는 AppendEntries 메시지이다. Client가 state를 변경하자고 leader에게 요청하면, leader는 새로운 로그를 만든다. 하지만 이 로그는 아직 state를 변경시킨 것은 아니다. Leader는 새 entry를 커밋하라고 follower들에게 AppendEntries 메시지를 보낸다. follower는 AppendEntries 메시지를 받으면 각자의 스토리지에 받은 로그를 커밋하고 leader에게 답변을 보낸다. Leader는 과반의 follower가 로그를 커밋했다는 메시지를 받으면, 자신도 로그를 커밋한다. 이렇게 leader에 log가 커밋된 뒤에야 state가 성공적으로 변경된 것이고, 클라이언트에게 요청이 처리됐다고 응답을 보낸다. AppendEntries 메시지는 follower가 가져야 할 로그들을 담고 있다. follower는 각자 저장하고 있는 로그의 상태가 다르기 때문에 follower들에게 보내야 할 로그의 양도 전부 다르다. 이는 leader가 nextIndex라는 이름으로 각 follower에 어떤 로그를 보낼지 저장하고 있다가 메시지를 보낼 때 사용된다. Follower가 할 일은 leader가 보낸 로그를 저장할 수 있는지 보고 leader에게 응답을 보내준다. Follower는 Ap

Raft - leader election

Raft에서는 모든 결정을 leader가 한다. 클라이언트의 모든 요청은 리더를 통해서만 가능하고, 새로운 로그를 추가하는 것도 새로운 노드가 추가되거나 기존의 노드를 지우는 것도 리더를 통해서 결정된다. leader의 명령을 따르는 노드들은 follower라고 하는데 follower들은 leader의 명령을 그대로 따른다. Follower는 leader가 보낸 명령에 따라 자신의 상태를 변경하고, 새로운 클라이언트가 접속하면, 클라이언트에게 어떤 노드가 리더인지 알려준다. Raft에서는 의도적으로 follower가 할 수 있는 일이 별로 없도록 만들었고 덕분에 프로토콜을 단순하게 만들 수 있었다. Leader인 노드는 일정 주기로 follower들에 heartbeat을 보낸다. follower들은 leader의 heartbeat을 듣고 있다가 일정 시간 동안 heartbeat을 듣지 못하면 leader가 죽었다고 생각하고 자신을 후보로 추천하며 다른 노드들에 자신을 leader로 뽑아달라고 RequestVote 요청을 보낸다. 이렇게 자신을 RequestVote 요청을 받은 노드를 candidate이라고 부른다. RequestVote를 받은 노드는 현재 자신의 상태를 보고 candidate이 더 최신 상태라면 새 leader를 지지하는 응답을 보내고, 그렇지 않으면 거절하는 응답을 보낸다. 반 이상의 노드가 자신을 지지한다고 응답하면 이 candidate은 leader가 된다. RequestVote를 보내고 일정 시간 동안 leader가 되지 못한 candidate은 다시 한번 모든 노드에게 RequestVote를 보낸다. 이때 얼마 만에 다시 RequestVote를 보낼지는 특정 범위 내에서 랜덤하게 결정된다. 랜덤한 timeout을 사용한다는 것은 Raft를 효율적으로 동작하게 하는데 매우 중요하다. 만약 고정된 시간을 사용한다면 모든 후보가 자기 자신에게 투표하라고 주장하며 선거가 끝나지 않을 수 있다. Candidate이 더 최신인지 아닌지는 te

Raft - 이해하기 쉬운 consensus algorithm

분산 시스템을 구축할 때, 모든 노드가 독립적으로 돌아가는 시스템을 설계한 것이 아니라면, 공유된 상태를 합의하기 위한 모종의 방법이 필요하다. 이런 식으로 분산 환경에서 상태를 공유하는 알고리즘을 consensus algorithm 이라고 한다. consensus algorithm 중에서 가장 유명한 알고리즘은 Paxos 다. 하지만 Paxos는 구현은커녕 이해하는 것 자체가 어렵기 때문에, Paxos를 구현한 라이브러리가 거의 없었고, 일부 분산 시스템들이 내부적으로 사용하는 정도였다. 그래서 분산 시스템을 구축할 때 현실적으로 사용할 수 있는 방법은 Zab algorithm을 사용하는 Zookeeper 를 이용하는 것이었다. Zookeeper는 매우 훌륭하다. 사실 지금까지도 분산 환경에서 consensus를 위해 사용할 수 있는 검증 된 거의 유일한 라이브러리라고 말할 수도 있을 정도다. 하지만 Zookeeper는 메시지의 순서가 보장돼야 하기 때문에 반드시 TCP를 사용해야만 한다. 또한, Zab algorithm이 Zookeeper의 구현과 긴밀하게 엮여 있기 때문에 다른 구현을 만들기 힘들어 반드시 JVM을 띄워야 하는 문제가 있다. Raft 는 구현체를 만들기 어렵다는 기존 consensus algorithm의 문제를 해결하기 위해 이해하기 쉬운 것을 최우선으로 설계된 consensus algorithm이다. Raft에서는 노드 간 공유될 state를 append-only state machine으로 본다. 따라서 노드 간에 상태가 합의됐다는 것은 이 state machine을 변경시키는 append command가 같은 순서로 적용됐다는 것을 의미한다. append command를 추가하는 것을 로그를 추가한다고 하고, 모든 노드가 같은 state를 가지게 하는 것을 log replication이라고 한다. 이때 어떤 append command를 추가할 것인지를 모든 노드가 일치한 정보를 가지는 것이 중요한데, Raft는 리더를

[CppCoreGuidelines] const_cast는 언제 써야 하는가

C++의 const_cast 는 레퍼런싱하는 object의 cv-qualifier를 제거하는 캐스팅이다. cv-qualifier는 타입에 constness와 volatility 를 더해주는 한정자이므로 const_cast 는 constness뿐 아니라 volatility도 제거할 수 있다. constness를 제거하면 수정할 수 없었던 object를 수정할 수 있게 해주고, volatility를 제거하면 해당 object에 접근하는 코드가 최적화되어 사라질 수 있게 해준다. 따라서 const_cast 는 실제 존재하는 object가 별도로 있고, 그것에 접근하는 방법을 변경한다. 문제는 volatile object의 레퍼런스를 const_cast 로 volatility를 없앤 뒤 이 object에 접근하는 것이나, const object의 레퍼런스를 const_cast 로 constness를 없앤 뒤 이 object를 수정할 경우 이 코드가 어떻게 동작할지는 undefined behavior 라는 것이다. 즉 아래와 같은 코드들은 전부 undefined behavior이다. 그렇다면 const_cast 는 언제 사용하는 것일까? 사실 const_cast 를 사용해야만 하는 경우는 없다. 무언가를 하기 위해 반드시 const_cast 가 필요하다고 느껴진다면 디자인에 무언가 문제가 있는 것이다. 그래서 C++ Core Guidelines 에서는 const_cast를 사용하지 않는 것 을 권장한다. 흔히 const_cast 를 사용하는 패턴을 정리하면 아래의 3가지 패턴으로 분류할 수 있다. 이제부터 그 3가지 패턴이 왜 사용하면 안 되는지 설명할 것이다. 첫 번째 용례는 어떤 object의 cv-qualifier를 cv 1 이고, 이 object를 레퍼런싱하는 cv 1 보다 더 cv-qualified 된 cv 2 를 가지는 변수를 가지고 있을 때, cv 1 보다 높고, cv 2 보다는 낮은 레벨의 cv-qualifier를 갖도록 하는 것이다. 즉, i

[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++] array to pointer decay

이미지
C++은 그 근본이 C에서 나왔기 때문에 C에서 많은 문제점을 물려받았다. 대표적으로 implicit conversion 이 그런데, 그중에서 array가 pointer로 implicit conversion 되는 것을 decay. 혹은 다른 decay들과 구분하기 위해 array to pointer decay라고 부른다. 이는 말 그대로 T[N] 타입으로 선언된 변수의 값이 T* 타입의 변수에 바인딩 될 때 implicit casting이 되는 것을 말한다. 이는 컴파일 타임 정보인 배열의 크기를 잃는다는 문제도 있지만, 그보다 더 큰 문제는 포인터에 대해서 upcasting 또한 implicit conversion이 가능하다는 데서 생긴다. 위와 같은 코드가 있다고 해보자. A 를 상속받은 B 클래스가 있고, B instance의 크기는 A instance의 크기보다 크다(정확히 따지자면 사용하는 alignment의 크기가 B 클래스보다 작은지 따져봐야 하지만, 여기서는 설명을 간단히 하기 위해 int 크기로 align 된다고 가정하고 진행하도록 하겠다). 이때 f 함수가 하는 일은 다음과 같다. B(3, 4) 를 생성한다. 1에서 생성한 객체의 A 부분만 slice 한 A(3) 을 인자로 받은 a 위치에 넣는다. B(5, 6) 를 생성한다. 3에서 생성한 객체의 A 부분만 slice 한 A(5) 를 인자로 받은 a 에 1 더한 주소에 저장한다. 앞에서 말했듯이 위의 코드는 일단 포인터는 실제 배열의 크기를 알지 못하기 때문에 f 함수가 받은 a 의 길이가 2 이상인지 알 수 없다는 문제가 있다. 하지만 이보다 더 큰 문제가 있다. 만약 f 함수를 아래와 같이 사용했다고 해보자. B[4] 타입인 a 는 B* 타입으로 decay 되고, B* 는 부모 타입인 A*로 upcasting 되어 f 함수에 넘어간다. f 함수를 실행한 뒤 사용자가 원했던 모습은 아마 위와 같은 모습일 것이다. 혹은 ob

[C++] Variadic template

전통적인 C나 C++에서 잘못 쓰면 위험한 것 중 하나로 stdarg 가 있었다. 이를 이용하면 임의의 개수의 인자를 받는 variadic function 을 만들 수 있지만, 함수의 시그니쳐를 통한 타입 체크를 완전히 무시하기 때문에, 함수의 호출자가 규약을 지키지 않고 함수를 사용하더라도 컴파일 타임에 문제를 잡을 수 없기 때문에 실행 시에 여러 가지 문제가 발생했다. 이런 문제를 피하고자 stdarg 를 사용하지 않고, 예상되는 인자의 최대 개수를 예상해서 받는 인자의 개수가 다른 함수를 오버로딩하여 사용하는 경우가 더 많았다. C++11에서는 variadic function을 타입 시스템과 붙여 컴파일 시간에 타입 체크 할 수 있게 하려고 variadic template이라는 문법을 추가하였다. Variadic template을 사용하면 사용된 인자의 타입 별로 instantiation 되기 때문에 바이너리의 크기는 커지지만, 컴파일 타임에 타입 체크를 할 수 있으며 실행 시에 생기는 비용을 줄여준다. Variadic template이 될 템플릿 인자는 ellipsis( ... )를 붙여 다음과 같이 사용한다. Variadic template을 사용하는 패턴은 크게 2가지이다. 첫 번째는 다른 함수로 인자를 전달하는 것이다. 대표적으로 make_unique 가 이런 경우에 속한다. Possible implementation 에서 볼 수 있듯이 make_unique 는 std::forward<Args>(args)... 를 이용해 T 타입의 생성자에게 모든 인자를 전달한다. 여기서 std::forward 함수 호출 뒤에 ellipsis가 붙어있다는 것이 중요하다. 이를 pack expansion이라고 하는데 std::forward 함수에 인자를 넘기는 패턴을 variadic arguments인 args 에 전부 적용하는 것이다. 따라서 args 가 N 개의 인자였으면 번의 std::forward 함수가 불리게 된다. 이때 단순히

c++에서 static 한정자가 하는 일

C++을 처음 배울 때 사람들이 많이 헷갈리는 것 중 하나가 static 이다. 사실 이건 배우는 사람의 문제도 아니고 가르치는 방법의 문제도 아니다. 그저 C++에서 static 을 다양한 용도로 사용하고 있기 때문이다. static storage static 키워드를 사용하는 한 가지 목적은 지역 변수에 할당할 객체를 static storage 로 만들기 위해서다. C++에서 block scope에 선언되는 변수가 가지는 객체는 기본적으로 automatic storage에 선언되어 block이 끝나면 자동으로 소멸한다. 하지만 간혹 특정한 범위 내에서만 접근할 수 있으면서 프로그램이 끝날 때까지 살아있는 객체, 다시 말해 지칭하는 변수는 block scope에 선언됐지만, static storage duration을 가지는 객체가 필요할 때가 있다. 이럴 때 static 한정자를 사용하면 된다. block scope에 선언되는 지역 변수에 static 한정자를 붙이면 이 변수가 가리키는 객체는 static storage duration을 가지고, block이 종료돼도 소멸하지 않는다. internal linkage static 한정자는 namespace scope에 선언되는 변수에도 사용될 수 있다. 하지만 namespace scope에 선언된 변수가 가지는 객체는 기본적으로 static storage에 할당된다. 따라서 namespace scope에 선언된 변수에 붙은 static 한정자는 객체가 할당될 storage에 영향을 주지 않는다. 여기서 static 은 다른 의미를 가진다. static 한정자는 namespace scope에서는 변수뿐 아니라 함수 선언에도 붙일 수 있다. 이 경우 선언된 변수나 함수의 linkage를 internal linkage 로 바꿔서 translation unit 밖으로 나가지 않도록 해준다. static member C++에서 static 이 가지는 마지막 의미는 클래스의 멤버 변수나 함

[C++] internal linkage와 external linkage의 차이

보통 프로그램은 하나의 소스가 아닌 여러 개의 소스로 구성된다. 이 소스를 C++ 표준에서는 translation unit이라고 하고, 소스를 컴파일하는 과정을 translation이라고 한다. translation의 마지막 단계는 link인데, 이 과정을 제외하고는 모든 translation은 독립적으로 진행된다. 이때 translation unit 내에서 이름이 어느 범위까지 사용될 수 있는가를 정하는 것이 지난번 글 에서 설명한 스코프이다. 이번에 설명할 linkage는 같은 이름이 같은 entity를 지칭하는 범위를 정하는 것이다. 각 이름은 어떤 linkage를 가지는가에 따라서 translation의 마지막 단계에서 어떻게 link 될지 달라진다. linkage에는 internal linkage와 external linkage 두 가지 종류의 linkage가 있다. 하지만 모든 이름이 linkage를 가지는 것은 아니다. 우선은 linkage를 가지지 않는 이름에 대해 알아보도록 하겠다. no linkage 영어로는 has no linkage 라고 하여 has internal/external linkage 와 자연스럽게 대구가 된다. 하지만 한국어로 말하면 no linkage를 가진다고 하는 것은 어색하므로 그냥 linkage를 가지지 않는다고 하는 것이 적당해 보인다. 어찌 됐든 모든 이름이 linkage를 가지는 것은 아니다. block scope에 선언된 이름은 함수와 extern으로 선언된 변수를 제외하면 전부 linkage를 가지지 않는다. 따라서 함수 안에서 선언한 로컬 클래스, enumeration, enumerator, 타입 등은 전부 linkage를 가지지 않는다. linkage를 가지지 않는 이름은 선언된 스코프에서밖에 접근할 수 없고, 스코프 밖에서 그 이름을 다시 사용해도 그건 다른 entity를 지칭하는 것이 된다. static 한정자를 사용하여 static storage에 저장한 경우도 마찬가지다. 이는 block

[C++] 이름과 스코프

C++에서는 많은 것들이 이름을 가질 수 있다. 변수는 모두 이름을 가지고, 객체는 변수를 통해서 이름을 가질 수 있다. 타입도 이름이 있고, 함수도 이름이 있다. 람다 함수는 익명 함수라는 번역 그대로 이름이 없지만, 변수에 할당하여 이름을 통해 접근할 수도 있다. 이 이름을 어디서부터 어디까지 사용할 수 있는가를 그 이름의 스코프라고 하는데, C++은 상황에 따라 다른 스코프를 사용한다. 이번 글에서는 C++의 이름이 가지는 스코프에 관해서 설명하도록 하겠다. block scope block scope는 선언된 문장부터 선언된 block이 끝날 때까지 사용할 수 있다. 블록 밖에서 선언된 같은 이름이 있으면 이름이 선언되기 전까지는 블록 밖에서 선언된 이름을 그대로 가지고, 선언된 문장부터 새로 선언한 의미를 가진다. 예를 들어 위와 같은 코드가 있을 때 4번째 줄까지 a 는 2번째 줄에서 선언된 a 이지만, 4번째 줄부터 block이 끝나는 10번째 줄까지는 4번째 줄에서 선언된 a 이다. 또한, block scope를 가지는 이름은 같은 block 안에서 다른 의미를 가지기 위해 다시 사용될 수 없다. 즉, 위에서 5번째 줄에서 10번째 줄까지 사이에서는 a 를 다른 의미로 사용할 수 없다. 스코프를 가지는 이름의 대표적인 예제로는 함수 안에서 선언된 변수인 지역 변수(local variable)가 있다. 이때 지역 변수와 automatic storage 에 저장되는 지역 객체를 혼동하면 안 된다. 스코프는 어디까지나 컴파일 시 그 이름을 어디까지 사용할 수 있는가 하는 것이지 객체의 라이프타임과는 상관없다. 지역 변수 중에서도 static 이나 thread_local 같은 storage를 지정해주는 specifier가 붙은 경우 이 객체들은 automatic storage에 저장되지 않는다. 지역 변수 외에도 typedef 나 using 으로 선언되는 type alias, 함수 안에서 선언되는 class 와 enum 같은 경우도 함

[C++] object는 언제 생성돼서 언제 소멸되는가 - storage

C++에서는 타입을 가지는 일정 크기의 값을 object라고 한다. object의 중요한 특징 중 하나는 lifetime. 즉, object가 언제 소멸하는가 하는 것이다. 이 object는 객체 지향 프로그래밍에서 말하는 객체와는 살짝 다른 개념이다. C++ 표준에서 object는 타입, alignment, lifetime 등의 특성을 가지는 일정 크기의 연속된 값을 의미한다. 이는 객체 지향 프로그래밍에서 말하는 객체와는 약간 다른 의미이지만 이 글에서는 그냥 객체라고 부르도록 하겠다. C++에서 객체의 lifetime을 알기 위해서는 우선 객체가 어디에 생성되는지를 알아야 한다. 일반적으로 C++ 개발자에게 객체가 어디에 생성되는지를 물으면 다음과 같이 대답할 것이다. 로컬 객체는 stack에, new로 생성된 객체는 heap에 저장되는데 stack과 heap은 같은 메모리 영역을 공유하며 stack은 메모리가 감소하는 방향으로 커지고, heap은 메모리가 증가하는 방향으로 커진다. 또한, 전역 변수 중 initialize 되지 않은 것은 bss에 initialize 된 것은 data 섹션에 저장된다. 이 대답은 기술적으로 맞는 대답이다. 사실 매우 훌륭한 대답이다. 실질적으로 대부분의 머신에서 대부분의 컴파일러는 위와 같은 방식으로 코드를 컴파일한다. 하지만 다시 한번 생각해보자. stack과 heap이 같은 메모리를 나누어 쓰는 이유는 한정된 크기의 메모리에서 컴파일 타임에 알 수 없는 두 종류의 동적 메모리를 할당하기 때문이고, bss 영역과 data 영역이 나누어지는 이유는 바이너리 파일의 크기를 줄이기 위한 최적화 때문이다. 즉, stack, heap, data, bss 등의 메모리 영역 등은 구현에 관련된 것일 뿐이다. 그렇다면 C++ 표준 문서에서는 이에 대해서 어떻게 기술하고 있을까? C++ 표준에서는 객체가 메모리에 생성된다고 하지 않고, 스토리지에 생성된다고 한다. 구현과 표준을 분리하기 위한 결정으로 구체적인 메모리 레이아웃

[C++] glvalue와 prvalue

지난번 글 에서 lvalue와 rvalue의 특징을 설명하면서 예고했듯이 이번에는 glvalue와 prvalue의 특징에 관해서 설명하도록 하겠다. 전에도 말했듯이 C++의 value category의 최하단에 있는 lvalue, xvalue, prvalue 중에서 lvalue와 xvalue가 glvalue에 포함된다. lvalue는 iM, xvalue는 im이고, prvalue는 Im이므로 glvalue와 prvalue를 나누는 기준은 i인지 아닌지. 즉, identity를 가지는지 여부이다. identity를 가진다는 것은 그 값이 expression을 넘어서까지 살아있다는 것이다. 그래서 identity를 가진다는 것은 그 값이 persist하다고 말하기도 한다. temporary object prvalue는 persist 하지 않다. 이는 prvalue인 expression이 의미하는 object가 특정한 스토리지를 점유하지 않는다는 것이다. 만약 스토리지에 할당될 필요가 있으면 prvalue는 temporary object를 생성한다. 생성된 temporary object는 레퍼런스 변수에 할당되지 않으면, expression이 끝나고 소멸한다. incomplete type prvalue는 구체화할 때 temporary object를 생성하므로, 컴파일 타임에 expression의 타입을 정확히 알아야 한다. 따라서 전방 선언만 돼있는 incomplete type인 prvalue는 있을 수 없다. 하지만 실행될 일 없는 decltype 안에 있는 expression의 경우 incomplete type인 prvalue가 있을 수 있다. polymorphic incomplete type의 prvalue가 있을 수 없는 것과 비슷하게 prvalue는 polymorphic 할 수 없다. 다시 말해서 prvalue인 object의 dynamic type은 그 expression의 static type과 항상 같다. persist 하지 않다

[C++] lvalue와 rvalue

지난번 글 에서 C++의 value category에 관하여 설명하였다. 요약하면 다른 값으로 move될 수 있는지와 identity를 가지는지에 따라서 lvalue, xvalue, prvalue로 나뉘고, lvalue와 xvalue를 합쳐서 glvalue, xvalue와 prvalue를 합쳐서 rvalue라고 부른다는 것이다. 앞에서도 말했듯이 C++의 value category를 나누는 기준은 move 될 수 있는지와 identity를 가지는지 여부이다. 이중 iM 은 lvalue라고 부르고, im 인 xvalue와 Im 인 prvalue를 합쳐 rvalue라고 부른다. identity를 가지지도 않고, move 될 수도 없는 IM 은 C++에 존재하지 않기 때문에 사실상 lvalue와 rvalue를 나누는 기준은 move 될 수 있는지 아닌지이다. 따라서 move 될 수 없으면 lvalue이고, move될 수 있으면 rvalue이다. 이번 글에서는 그래서 lvalue와 rvalue가 구분되는 특성을 설명할 것이고 다음 기회에 glvalue와 prvalue가 구분되는 특성을 설명하도록 하겠다. overloaded function 우선 lvalue와 rvalue의 가장 중요한 차이는 overload 된 함수가 있을 때 어떤 함수가 호출될지가 달라진다는 것이다. 위처럼 같은 타입의 lvalue reference를 받는 함수와 rvalue reference를 받는 함수가 overload돼있을 때, argument가 lvalue이면 lvalue reference를 받는 함수가 호출되고, rvalue이면 rvalue reference를 받는 함수가 호출된다. 따라서 위와 같이 사용했을 때, func(a) 는 a 가 lvalue이므로 0 이 리턴되지만, std::move(a) 는 xvalue이고 1 은 prvalue이므로 func(std::move(a)) 와 func(1) 은 1 이 리턴된다. universal reference rvalue re

왜 c++은 복잡한 value category를 가지게 됐는가

C++11이 나오기 전, C++의 value category는 lvalue와 rvalue만으로 이루어진 단순한 분류체계를 가지고 있었다. 하지만 C++11이 나오면서 xvalue, glvalue, prvalue가 추가되면서 복잡한 분류체계를 가지게 됐다. 이번 글에서는 C++11에서 변경된 value category에 대해서 알아보도록 하겠다. until c++03 C++11에서 새로 추가된 value들을 알기 전에 C++03 이전에도 있었던 lvalue와 rvalue가 뭔지부터 확실히 하고 넘어가야 한다. 흔히들 많이 실수하는 것이 lvalue는 assign operator(operator =)의 왼쪽에 올 수 있는 값이고, rvalue는 올 수 없는 값이라고 생각하는 것이다. C에서 lvalue라는 개념이 처음 나왔던 시절에는 assign operator를 기준으로 lvalue인지 아닌지를 구분하는 것이 맞았다. 하지만 C89에서 const 한정자가 추가되면서 더는 맞지 않다. const variable은 lvalue이지만 assign operator의 왼쪽에 올 수 없기 때문이다. 따라서 C89에서는 lvalue를 left value가 아닌 locator value. 즉, 실제 값이 아닌 값이 있는 주소를 지칭하는 locator value라고 정의했다. 즉, 변수는 locator이기 때문에 lvalue이고, 1, 2 같은 숫자 리터럴이나 'a', 'b' 같은 문자 리터럴, 혹은 함수의 실행 결괏값 같은 경우 특정한 주소를 지칭하는 locator가 아니므로 lvalue가 아니다. 이 중에서 const 한정자가 붙지 않은 경우를 modifiable lvalue라고 따로 분류하여, modifiable lvalue만 assign operator의 왼쪽에 올 수 있다. 후에 나온 C++98도 C89의 lvalue 정의를 따랐기 때문에 C++98에서의 lvalue도 locator 라고 보면 된다. 다만 C89와 다르게 레퍼런스를

round - 실수를 정수로 근사하기

지난번 글 에서 round라는 것은 반올림이 아니고 근삿값을 구하는 방법이기에 다양한 방법이 있다고 설명했었다. 하지만 프로그래밍에서 round 함수는 대부분 실수를 정수로 만드는 round이다. 그래서 이번 글에서는 실수를 정수로 근사하는 방법들을 설명하도록 하겠다. 우선 실수를 정수로 만드는 근사법 중에서 가장 유명한 것은 ceil 과 floor 이다. 이는 각각 round up 과 round down 이라고 부르기도 하는데 이를 번역하여 한국어로는 올림과 내림이라고 부른다. 번역 그대로 ceil 은 근삿값을 구하는 자릿수에서 값을 올리고, floor 는 내린다. 따라서 ceil(2.5)는 3이 되고 floor(2.5)는 2가 된다. 또한, 이 둘은 양의 무한과 음의 무한을 향해 가는 모습이기 때문에 각각 round towards positive infinity 와 round towards negative infinity 라고 불리기도 한다. 이와 구현이 비슷한 것으로 truncate 가 있다. truncate 는 보통 버림이라고 번역되는데, 이는 근삿값을 구할 자릿수 아래의 값을 버린다. 따라서 truncate(2.5)는 2가 된다. 이는 floor 와 같아 보이지만 다른 구현이다. 예를 들어 floor(-2.5)의 경우 이는 음의 무한을 향해 가기 때문에 -3이 되지만, truncate(-2.5)는 자릿수를 버려 -2가 된다. 이는 0에 가까운 수를 고르는 것 같은 동작을 하므로 round towards zero 라고 부른다. 이와는 반대로 0에서부터 멀어지는 방향으로 움직이는 round away from zero 라고 부르는 방법도 있지만, truncate 에 비해 딱히 특색이 있지는 않아서 잘 쓰이지 않는다. 지금까지 설명한 ceil , floor , truncate , round away from zero 는 모두 큰 쪽이거나 작은 쪽이거나 한 방향으로 움직인다. 하지만 이보다 더 많이 사용되는 것은 가까운 정수( to nearest

round(1234.5)의 결과는 무엇일까

혹시 1235라고 생각했는가? 그렇다면 아마 다음과 같은 과정을 거쳤을 것이다. round 는 주어진 실수를 정수로 반올림하는 것이다. 반올림은 가장 가까운 수를 만드는 것이다. 반올림할 자릿수가 0, 1, 2, 3, 4이면 내리고 5, 6, 7, 8, 9이면 올린다. 따라서 1234.5의 5는 올려야 하므로 답은 1235이다. 만약 이렇게 생각했다면 round 가 무엇인지 제대로 이해하지 못한 것이다. round 가 무엇인지 알기 위해 우선 round 의 정의부터 확실히 해보자. 흔히들 착각하는 것이 ceil 은 올림, floor 는 내림, round 는 반올림이라고 생각하는 것이다. 하지만 round 는 반올림이 아니다. Wikipedia에서 말하는 round 의 정의는 다음과 같다. Rounding a numerical value means replacing it by another value that is approximately equal but has a shorter, simpler, or more explicit representation. round 란 것은 반올림이 아니라 근사값을 구하는 것이다. 즉, 반올림을 구하는 것도 round 이지만, 올림과 내림도 round 고, 1234 5678 같은 복잡한 분수를 1 5 로 간단하게 표현하는 것도 round 이다. 또한, 반드시 일의 자리까지 round 할 필요도 없다. 물론 보통의 round 구현은 실수를 입력받아 정수 근사값을 만든다. 하지만 반드시 이럴 필요는 없다. 경우에 따라서 십의 자릿수 까지 근사값을 만들 수도 있고, 백의 자릿수 까지 근사값을 만들 수도 있다. 즉, round ( 1234.5 ) 의 답은 1000이 될 수도 있고, 1200이 될 수도 있고, 50 단위로 근사값을 구한다면 1250이 될 수도 있다. 따라서 round 를 어떻게 구현했는지 사용하는 라이브러리의 스펙을 확인해봐야 한다.

JavaScript와 IEEE 754

JavaScript는 표준에서 숫자 타입은 IEEE 754-2008, 64 bit format을 따른다 고 명시돼있다. 따라서 숫자 타입의 연산도 IEEE 754-2008을 따를 거로 생각했다. 하지만 ECMAScript 명세에서 NaN 이나 Infinity 를 포함한 연산에 대해 다른 결과를 내도록 정의한다. 그 대표적인 경우가 1 ∞ 과 -1 ∞ 이다. IEEE 754-2008는 지수를 계산하는 방법을 3가지 정의한다. 첫 번째는 pown 으로 지수가 정수인 경우에 대해 정의돼있고, 두 번째 pow 는 밑과 지수가 모든 실수인 경우 사용할 수 있도록 정의돼 있고, 마지막은 powr 으로 밑이 0 이상의 실수인 경우에 대해서 정의돼 있다. 우선 첫 번째인 pown 는 지수가 ∞ 가 되지 못하므로 관심 대상이 아니다. 다른 두 지수 함수인 pow 와 powr 은 1 ∞ 에 대해 다른 결과를 내도록 정의한다. pow 는 1 을 리턴하고, powr 는 invalid operator exception 을 발생하도록 정의했다. 사실 이는 양쪽 다 이상한 것은 아니다. lim x → ∞ 1 x 은 1 에 수렴하지만 1 ∞ 자체는 부정이기 때문에 1 인 경우와 invalid operator exception 이 발생하는 경우 양쪽 모두 말이 되기 때문이다. 그렇다면 IEEE 754-2008은 -1 ∞ 를 어떻게 정의할까? 이 경우 밑이 0 보다 작으므로 pow 만이 유효한 함수고, 이에 대해서 1 을 리턴하도록 정의했다. lim x → ∞ -1 x 는 발산하고, -1 ∞ 는 부정이기 때문에 이는 수학적으로 올바른 정의가 아니고, IEEE 754에서 어떻게 이렇게 정했는지는 모르겠다. 역사적인 이유이거나 이렇게 할 경우 구현이 편해지기 때문일 것인데 혹시 정확한 이유를 알고 있는 사람이 있으면 알려주기 바란다. ECMAScript에서는 1 ∞ 와 -1 ∞ 의 연산에 대해서 NaN 을 리턴하도록 정의했다. 이는 ECMAScript는 1997년 만들어졌다.

[c++] 생성자에서 예외가 발생하면 어떻게 될까

R.A.I.I. 를 사용하다 보면, 생성자에서 복잡한 일을 해야 할 경우가 종종 생긴다. 복잡한 일은 실패할 수도 있고, 그 경우 예외가 발생하기도 한다. 여기서 궁금한 점이 생긴다. 생성자에서 예외가 던져져도 아무 문제 없을까? 우선 걱정되는 것은 메모리 릭이다. 하지만 다행히도 메모리 릭은 발생하지 않는다. 스택에 생성된 변수는 스택 unwind를 통해 메모리가 해제되며, new operator를 통해 힙에 할당하다 예외가 발생한 경우에도 new operator가 알아서 메모리를 해제하고 nullptr 를 리턴해준다. 그다음으로 걱정되는 것은 멤버 변수의 소멸자가 잘못 불리지 않을까 하는 것이다. 하지만 이 역시 문제없다. 예외가 발생한 시점에서 멤버 변수는 초기화가 완료된 멤버 변수, 초기화 중이었던 멤버 변수, 초기화되지 않은 멤버 변수 3가지로 나눌 수 있다. 초기화가 완료된 멤버 변수는 말 그대로 생성자가 불렸고 정상적으로 메모리 할당을 완료한 멤버 변수다. 위 코드에서는 b 에 해당하는데, 이들은 예외가 발생하면 이 변수들은 정상적으로 소멸자가 불리며 리소스를 해제한다. 초기화 중이었던 변수는 위 코드에서 c 에 해당하는 멤버 변수다. 위 코드는 E 클래스의 생성자에서 C 인 변수 c 의 생성자가 예외를 발생시켰다. 이 경우 E 클래스 입장에서 c 는 초기화 중인 변수가 된다. 위 코드와 같이 멤버 변수의 초기화 중에서 예외가 발생하면 초기화 중인 변수가 1개 존재하지만, 생성자의 본체에서 예외가 발생하면 초기화 중인 변수는 존재하지 않는다. 마지막으로 초기화되지 않은 변수의 경우 생성자가 불리지 않았으니 소멸자가 불리지 않는다. 하지만 이 경우는 아직 생성자가 불리지 않았기 때문에 소멸자가 안 불리는 것이 맞다. 상속받는 경우는 어떨까? 부모 클래스에서 할당한 리소스는 정상적으로 해제될까? 당연히 이 경우도 아무 문제없다. 생성자에서 예외가 발생했을 때, 초기화가 완료 된 멤버 변수의 소멸자가 전부 불리고 나면, 부모 클

[Python] negative index를 사용하자

일반적으로 random access 가능한 자료 구조에서 뒤에서 k 번째 원소를 가져오는 방법은 다음과 같다. 자료구조의 크기를 가져온다. 크기를 기반으로 접근할 인덱스를 구한다. 원하는 자료를 가져온다. 이를 코드로 표현하면 다음과 같다. 파이썬에서는 이를 보다 간결하게 표현하기 위해 negative indexing을 지원한다. negative indexing은 말 그대로 음수를 이용해서 뒤에서부터 자료에 접근하는 것이다. negative indexing을 이용하면 위의 코드는 아래와 같이 표현된다. 파이썬에서는 뒤에서부터 접근할 때 negative indexing을 사용하는 것이 권장되지만 다른 언어로 코딩을 처음 배운 사람들은 파이썬에서도 길이를 가져와 인덱스를 계산하는 방식을 사용하는 경우가 종종 있다. 이런 사람들은 negative indexing이 단순한 syntactic sugar 라고 생각하는 것 같다. 하지만 negative indexing은 syntactic sugar가 아니다. negative indexing이 syntactic sugar라면 negative indexing을 쓴 것과 같은 바이트 코드가 생성돼야 한다. 하지만 둘은 서로 다른 바이트 코드가 생성된다. 위의 각 코드가 실제로 생성하는 바이트 코드는 다음과 같다. 첫 번째 코드는 global memory에서 len 함수를 찾아 호출하여 그 결과에 k 를 뺀 값을 인덱스로 사용하는 코드를 생성하고, 두 번째 코드는 k 를 음수로 만들어 바로 인덱스로 사용한다. 인덱싱을 통해 값을 가지고 오는 바이트 코드는 BINARY_SUBSCR 인데, 물론 인터프리터가 BINARY_SUBSCR 를 실행할 때 주어진 인덱스가 음수이면 내부적으로 리스트의 길이를 가져와 인덱스를 다시 계산 하는 과정을 거친다. 하지만 Global Interpreter Lock(a.k.a. GIL) 때문에 이 둘의 동작은 같지 않다. 파이썬의 GIL은 바이트 코드 단위의 atomi

Python은 어떻게 swap하는가

지난번 글 에서 Lua의 multiple assignment를 이용한 swap이 내부적으로 어떻게 돌아가는지 설명했었다. 그렇다면 파이선은 어떨까? 자신의 버추얼 머신의 구현까지 제공하는 루아와 다르게 파이선은 공식적으로 어떤 버추얼 머신을 사용해야 하는지 제공하지 않는다. dis 라이브러리를 통해서 어떤 바이트 코드가 나오는지 알 수 있지만, 구체적인 버추얼 머신의 스펙을 정의하거나 바이트 코드의 구현을 정의하지 않는다. 덕분에 PyPy , IronPython , Jython 등 다양한 구현체가 존재한다. 이번에는 우선 그중에서 사실상 표준 구현체라고 할 수 있는 CPython의 구현에 대해서 살펴보겠다. CPython의 버추얼 머신이 어떻게 구현돼야 하는가는 명확히 기술되지 않았지만, CPython의 바이트 코드를 보면, CPython은 Lua가 레지스터 머신을 사용하는 것과 다르게 스택 머신을 사용한다는 것을 쉽게 알 수 있다. CPython의 버추얼 머신은 스택과 글로벌/로컬 메모리를 가지고 있다. 글로벌 메모리와 로컬 메모리는 객체의 레퍼런스를 저장하는 데 사용되고 계산을 하기 위해서는 스택으로 레퍼런스를 복사해온 뒤 사용해야 한다. 이제 swap이 CPython에서 어떤 바이트 코드로 컴파일되는지 살펴보자. 위와 같은 코드는 아래와 같은 바이트 코드로 컴파일된다. 여기서 ROT_TWO 는 스택의 가장 위의 두 아이템의 위치를 바꿔주는 바이트 코드다. 즉, 위의 바이트 코드는 스택에 a 를 올리고 b 를 올린 뒤, 스택의 가장 위의 두 아이템의 위치를 바꾸고, 스택 가장 위의 아이템을 b 에 다음 아이템을 a 에 저장하는 것이다. 그렇다면 ROT_TWO 는 어떻게 구현됐을까? 현재 CPython에서 ROT_TWO 는 임시 변수 2개를 사용하여 스택의 두 값의 위치를 바꾸는 것으로 돼 있다. 즉, CPython에서는 변수 2개를 스왑하기 위해서 스택의 두 자리, ROT_TWO 에서 사용하는 임시 변수 두 자리까지 총 6개의 의

Lua를 쓰면 3번째 의자가 필요하지 않을까 - lua는 어떻게 swap할까

프로그래머들 사이에서 자주 하는 농담 중에 프로그래머 두 명이 자리를 바꾸려면 의자가 3개 필요하다는 농담이 있다. 둘이 동시에 자리에서 일어나 자리를 옮길 수 있는 사람과 달리 컴퓨터가 다루는 값은 언제나 어딘가에는 할당돼 있어야 해서 나온 말이다. c++로 작성하면 아래와 같은 코드가 되는데, 여기서 local 변수인 temp 가 3번째 의자가 된다. 그렇다면 multiple assignment 가 가능한 lua같은 언어는 어떨까? lua로 swap 하는 함수를 짜면 위와 같은 코드가 된다. 일단 겉보기에는 3번째 의자가 필요 없어 보인다. 하지만 정말로 3번째 의자가 필요 없을까? 어떤 트릭을 쓰기에 그런 것이 가능한 것일까? 일단 결론부터 말하면 그런 마법은 없다. 위의 코드가 루아 바이트 코드로 컴파일되면 다음과 같은 바이트 코드가 나온다. 이 코드를 이해하기 위해 우선 루아 버츄얼 머신을 이해해야 한다. 루아 버츄얼 머신은 stack-based가 아니라 register-based 머신이다. 루아 버츄얼 머신은 최대 256개의 레지스터를 사용할 수 있으며, 함수의 인자는 순서대로 0번 레지스터부터 할당된다. 물론 이는 어디까지나 버츄얼 머신이 이렇다는 것이기 때문에 실제로 256개의 레지스터가 필요하지는 않고, 적절히 번역돼서 실행된다. 이제 위의 바이트 코드를 읽어보자. b, a = a, b 는 MOVE 2 0; MOVE 0 1; MOVE 1 2 로 컴파일됐다. 이를 이해하기 위해서는 MOVE x y 는 x번째 레지스터에 y번째 레지스터의 값을 복사해 넣는 바이트 코드라는 것과 인자로 넘어온 a 와 b 는 각각 0번 레지스터와 1번 레지스터에 저장돼 있다는 것을 알아야 한다. 그러면 위의 바이트 코드는 3번째 의자인 2번 레지스터를 이용해서 두 변수를 스왑한다는 것을 알 수 있다.

이 블로그의 인기 게시물

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

RAII는 무엇인가

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

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

[Web] SpeechSynthesis - TTS API