Posts

클린코드1 - 좋은 코드란 무엇일까?

좋은 코드란? 내가 생각하는 좋은 코드는 이해하기 쉽고 바꾸기 쉬운 코드이다. 이해하기 쉬운 코드 내가 회사일을 할 때 보면 코드를 작성하는 것보다 코드를 읽는데 최소 수십 배 이상의 시간을 쓴다. 코드를 짤 때는 연관 코드를 읽고 짜야하고, 내가 짠 코드에서 버그가 발생하면 내가 짠 코드뿐만 아니라 연관 코드를 다 봐야 한다. 또한 다른 사람이 짠 코드에서 발생한 버그를 디버깅해야 할 때도 많다. 이렇게 코드를 짜는 시간보다 읽는 시간이 훨씬 많기 때문에 이해하기 쉬운 코드가 중요하다. 바꾸기 쉬운 코드 버그를 고치면서, 아니면 일부 기능을 조금 바꾸면서 버그를 만든 적이 많지 않은가? 나는 그런 적이 많다. 내가 부주의해서 그런 적이 많아 매번 반성하며 앞으로 그러지 않으려고 노력한다. 하지만 바꾸기 어려운 코드가 내 부주의함과 합쳐져서 버그를 만든 경우가 많다. 두 군데만 바꾸면 될 줄 알았는데, 중복 코드가 있어서 세 군데를 바꿔야 한다던가 등 말이다.  관련 책 내용 클린 코드 1.3 나쁜 코드로 치르는 대가, 클린 코드란? (여러 사람의 인터뷰 중 가장 와닿는 것만 가져옴) 데이브 토마스(OTI 창립자이자 이클립스 전략의 대부) 클린 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 위드 커닝엄(위키 창시자, 피트 창시자, 익스트림 프로그래밍 공동창시자, ...) 코드를 읽으며 짐작했던 기능을 각 루틴이 그대로 수행한다면 클린 코드라 불러도 되겠다. 1.4 저자 생각 코드를 읽는 시간 대 코드를 짜는 시간 비율이 10:1을 훌쩍 넘는다. 새 코드를 짜면서 우리는 끊임없이 기존 코드를 읽는다. 비율이 이렇게 높으므로 읽기 쉬운 코드가 매우 중요하다. 비록 읽기 쉬운 코드를 짜기 쉽지 않더라도 말이다. 하지만 기존 코드를 읽어야 새 코드를 짜므로 읽기 쉽게 만들면 사실은 짜기도 쉬워진다. 실용주의 프로그래머 Tip14 좋은 설계는 나쁜 설계보다 바꾸기 쉽다. 바꾸기 더 쉽게(Easier to Change. ETC) 이게 전부다. 왜 결합도를 줄이면

C코드 최적화 - 전역변수 대신 지역변수 사용

반복문 등에서 반복적으로 사용되는 변수 등은 전역변수 말고 지연변수를 사용하는 게 빠르다. 지역변수는 CPU 레지스터에서 바로 사용되나, 전역변수는 RAM에서 레지스터로 읽어와서 결과를 쓰기 때문이다. 『 레지스터 32bit CPU인 경우에, CPU에는 여러 연산(더하기, 빼기 등)을 처리할 수 있는 연산기가 있고, 내부에는 32bit 레지스터(register)가 여러 개 있다. 레지스터는 CPU가 사용하는 조그마한 저장 공간이다. CPU는 레지스터 저장공간을 사용해서 연산을 한다. 예컨대, c = a + b; 라면 CPU는 a, b를 각각 레지스터0, 레지스터1에 올려 놓고 더하기 연산을 해서 레지스터2에 더하기 결과를 넣을 수 있다. 』 int global_sum = 0; int calculate_sum_by_global_variable(void) { for (int i = 0; i < 10; i++) { global_sum += get_some_value(i); } } void calculate_sum_by_local_variable(void) { int local_sum = 0; for (int i = 0; i < 10; i++) { local_sum += get_some_value(i); } global_sum = local_sum; } 위의 C코드 예제를 보면 지연변수를 사용하는 게 더 느릴 거라고 생각할 수도 있다. C코드상으로는,  전역변수 사용 시에는 전역변수에 바로 합계를 계산하는 반면에,  지역변수를 사용 시에는 지역변수를 선언하고 합계를 계산한 후에 전역변수에 대입하기 때문이다. 하지만 CPU가 동작할 때는, 지역변수 사용 시에는 [①레지스터에 할당된 지역변수에 더하기]를 10번 한 후에 [②전역 변수(RAM)에 대입]하는 반면에, 전역변수 사용 시에는 [①전역변수를 RAM에서 읽어서 레지스터에 넣고 ②더하기를 한 후에 ③레지스터를 전

C코드 최적화 - char, short 대신 int 사용

0~100의 값만 필요한 변수라면 성능 관점에서 char, short, int 중에 어느 걸 써야 할까? int를 써야 한다. int 대신에 char, short 을 사용하면 성능이 떨어지는 경우가 있기 때문이다. 먼저 CPU 동작을 매우 간단하게만 알아보자. 32bit CPU인 경우에, CPU에는 여러 연산(더하기, 빼기 등)을 처리할 수 있는 연산기가 있고, 내부에는 32bit 레지스터(register)가 여러 개 있다. 레지스터는 CPU가 사용하는 조그마한 저장 공간이다. CPU는 레지스터 저장공간을 사용해서 연산을 한다. 예컨대, c = a + b; 라면 CPU는 a, b를 각각 레지스터0, 레지스터1에 올려 놓고 더하기 연산을 해서 레지스터2에 더하기 결과를 넣을 수 있다. 그런데, 32bit인 int는 레지스터에 그대로 넣으면 되는데, 8bit인 char, 16bit인 short은 레지스터에 어떻게 넣을까? 부호 있는 수(signed)이면 부호확장(sign extension)하고 부호 없는 수(unsigned)이면 제로확장(zero extension)해서 레지스터에 넣는다. 포스팅 ▶ 부호 확장(sign extension)과 제로 확장(zero extension) int 대비 그만큼 추가 연산이 발생해서 성능에 불이익이 생긴다. (CPU마다 명령어(instruction)가 다르므로 추가 연산이 없는 CPU도 있을 수도 있지만 일반적으로는 추가 연산이 발생한다.) 아래에서 추가 연산이 발생하는 C코드를 보자. unsigned char increase_char(unsigned char param) { param = param + 1; // param = param + 1; // param = param & 0x000000ff; return param; } unsigned short increase_short(unsigned short param) { param = param + 1; // pa

__builtin_expect (likely, unlikey) 설명 / 성능 차이

GCC의 __builtin_expect keyword는 분기문이 어디로 탈지 컴파일러에게 hint를 줘서, 컴파일러가 분기문 최적화를 하도록 한다. __builtin_expect가 뭔지 알아보고 __builtin_expect가 분기문 최적화를 어떻게 하고 성능 차이는 어떻게 되는지 어셈블리 코드로 알아보자. * 확인 방법 : C 코드를 컴파일하여 생성된 RISC-V 어셈블리(assembly) 코드로 확인 포스팅 ▶ 테스트 조건 및 성능 / 메모리 사용량 비교 방법 포스팅 ▶ RISC-V 어셈블리 명령어 설명 __builtin_expect 관련 사항 __builtin_expect __builtin_expect는 GCC 내장함수이다. prototype : long __builtin_expect (long exp, long c); if (exp) foo; 로 변환되며, 컴파일러는 exp == 0 으로 예상해서(foo() 함수를 타지 않을 것이라 예상) 분기문 최적화를 한다. likely, unlikely linux kernel에서는 아래와 같이 define 하여 사용한다. #define likely(x)       __builtin_expect(!!(x), 1) #define unlikely(x)     __builtin_expect(!!(x), 0) [[likely]], [[unlikely]] C++20에서 likely, unlikely attributes가 추가되었으며, syntax는 [[likely]], [[unlikely]]이다. 사용법은 GCC _butin_expect와 조금 다르나 마찬가지로 분기문이 어디로 탈지 컴파일러에게 알려주어, 컴파일러가 분기문 최적화를 한다. 분기 예측 __builtin_expect를 사용하면 컴파일러는 CPU의 정적 분기 예측에 맞춰서 최적화를 한다. 따라서 분기 예측을 간단히 알아보자. 현대 CPU는 성능을 위해 대부분 파이프라인(pipeline)을 사용하며, 이로 인해 분기문에서 파이프라인에 stall이 걸려서 성능이 감소

C언어 volatile keyword

C언어에서 volatile을 변수에 적용하면 컴파일러가 최적화를 하지 않고 항상 해당 메모리에 접근한다. volatile의 사전적인 의미는 "변할 수 있는" 정도의 뜻이며, 변할 수 있는 값이기 때문에 컴파일러가 최적화를 할 수 없다. volatile은 HW 접근 시에 보통 사용한다. 아래의 예제를 보자. * target CPU는 RISC-V이며 컴파일 조건은 아래와 같다. 포스팅 ▶ 테스트 조건 및 성능 / 메모리 사용량 비교 방법 * RISC-V 명령어 설명은 아래를 참조하자. 포스팅 ▶ RISC-V 어셈블리 명령어 설명 <C 코드> #define HW_QUEUE_POP (*(int*)0x20000000) int first_value; int second_value; void get_two_values(void) { first_value = HW_QUEUE_POP; second_value = HW_QUEUE_POP; } <어셈블리 코드> 00000000 <get_two_values>: 0: 200007b7 lui a5,0x20000 4: 0007a783 lw a5,0(a5) # a5 = 20000000번지의 값 8: 10000737 lui a4,0x10000 c: 00f72223 sw a5,4(a4) # 10000004번지(first_value) = a5 10: 10000737 lui a4,0x10000 14: 00f72023 sw a5,0(a4) # 10000000번지(second_vaule) = a5 18: 00008067 ret Disassembly of section .sbss: 10000000 <second_value>: 10000000: 0000 unimp ... 10000