[t:/]$ 지식_

C가 빠른 이유와 오해.

2019/04/26

문득 썼는데, 틀린 내용이 있을 수 있고, 선언이 아니고 의견이므로 다름과 틀림은 서로 빻음 관계이므로 틀린 것을 다르다고 하지말고 빻은 것을 틀리다 하면 안 되겠습니다. 늘 그렇듯이 빠르게 썼으므로 그 정도의 수준입니다..

...

C로 짜서 빨라요~ 는 사실 중요한 이유가 못된다. 숙련 자바 개발자도 얼마든지 빠른 프로그램을 짤 수 있다. 이건 사실 도메인의 문제다.

C가 빠른 이유는 C로 먹고사는 C개발자들이 대부분 C개발자처럼 생각한다는 점에 있다. 주로 그런 도메인이다. 큰 프로그램 보다는 빠른 프로그램이다. 큰 소프트웨어인데 빨라야 하는 영역도 있다. OS, 게임(그래픽/네트워크), 고속/분산 네트워크 처리(이건 좀 아닐 수도..) 같은 영역이 그런데, 그런쪽 굇수들은 따로 있다. 굇수가 되어야 한다.

몇 줄 써보니 C 개발자처럼 생각하는 C 개발자들이 얼마나 있는지, C 개발자들이 C 개발자처럼 생각하는 영역에서 C 개발자로 활동하는 개발자가 얼마나 있는지는 약간 비관적이다.. 그런 도메인도 많이 남지 않은 것 같다. (아차차 저는 그저 월급충이라 잘 모릅니다..)

여튼.. C 개발자들이 생각하는 방식을 생각나는데로 몇 가지만 써보면,

1. 컨텍스트 스위칭을 줄이거나 의도적으로 만든다.

= 속도를 올리거나 CPU 잠식을 막거나, 쓰레드 동시성 효율을 올리기 위함이다.

2. 불필요한 memcpy를 줄인다.

= 대부분의 데이터는 레퍼런스로 참조한다. 포인터만 찍는다.

3. 로캘리티를 높인다.

= 캐시 힛트가 증가하고 컴파일러가 레지스터를 재사용하거나, 자동으로 SIMD 코드를 만들어준다.

4. 분기 회피

= 파이프 스톨 방지

5. 얼라인 최적화

= 네트워크 오더링을 위한 불필요한 스왑을 막거나, 하드웨어 얼라인이 빈약하거나 없는 임베디드 시스템을 위한 것이다. 또는 캐시라인 바운더리를 맞추기 위함이다.

6. 거짓 공유 방지

= 캐시 라인에 있어서 멀티 쓰레드 최적화를 위함이다.

7. 커널-사용자 메모리 복사 회피

= 메모리 복사 비용과 CSW를 동시에 막는다.

8. 배열식 접근의 선호, 해시 접근.

= 캐시 힛트가 증가하고, 참조를 위한 점프를 막아서 파이프 스톨을 방지한다. 루프는 컴파일러가 깨준다. 오프셋을 키처럼 써서 접근하는 방식은 자료구조의 탐색이 발생하지 않는다.

9. SIMD 최적화

= 두 말 할 것도 없는 마법

10. 마른 수건도 쥐어짜기

= 그냥 습관이다.

11.비트, 바이트 쥐어짜기, 분기 쥐어짜기, 루프 쥐어짜기, 스윗치 쥐어짜기, 시프트 연산.

12. 디스어셈블, 가용 레지스터 총동원

= O3도 삽질하는 경우는 많다. SIMD의 경우 컴파일러가 의외로 잘 해줄 때도 있고, 등신 같을 때도 있다.

13. 자료구조 만들어쓰기

= 커널 소스에 있는 rbtree등을 이용하는 것은 매우 좋은 선택이다. 가용 크기가 예측된다면 정적 사전 할당으로 만들어 놓는 쪽이 빠르다.

14. 빠른 루프/분기 탈출

= 노는 루프를 피하는 것은 습관이다.

15. 락 회피

= 대부분의 쓰레드 작업은 공용부를 제거하여 각자의 루프가 최선을 다할 수 있도록 작성된다. 공용부가 있더라도 공용부 전체를 크리티컬 섹션화 하지 않고 인덱스 정도로 줄인다. CAS나 락프리 자료구조, 트랜잭셔널 메모리등에 대한 이해가 있다면 더욱 좋으(...나 나는 모름..)

16. 락 종류의 선택

= C 개발자가 처리하는 업무의 많은 영역이 스핀락 만으로 커버할 수 있으며 스핀락이 CPU를 잠식하지는 않도록 개발한다.

17. 남는 CPU의 활용

= 멀티 쓰레드에 있어서 남는 CPU를 최대로 활용할 수 있거나, 의도적으로 CPU를 남겨두어 버퍼를 만들거나 한다.

18. IO/CPU 트레이드 오프 계산

= IO/CPU중 노는 쪽을 더 많이 활용할 수 있도록 고안한다.

19. mmap의 활용, 페이지 폴트 방어

= 커널 캐시를 최대로 활용하되, 페이지 폴트 오버헤드는 막는 쪽으로 연구한다.

20. 파일시스템의 원리 탐구

= 때로 하드디스크 헤드 이송간 거리를 고려하거나, SSD의 블럭 머지/스플릿 비용을 고려하거나, NAND의 시퀀설 리드 성능을 고려하기도 한다.

21. 워드와 바이트

= 워드 단위로 처리해서 레지스터 사용률을 더 많이 올릴지 고민할 수도 있고, 암썸 인터워킹을 고려할 수도 있으며, avx를 나눠서 더 빠르게 처리해볼 수도 있다.

22. CPU 명령어 탐구

= fma와 같은 삼항 명령어를 잘 쓰는 것도 고려대상이다. NUMA냐 UMA냐 따져서 캐시 힛팅과 주소 나눔에 대한 고민을 할 수 있다. 인텔 데이터시트의 CPI 같은 클럭 비용도 당연히 살펴보곤 한다.

23. 정적 할당과 동적 할당

= 요구 데이터 크기가 예측안에 있다면 정적 할당을 고려한다. 동적 할당의 경우에도 통계적 사용 빈도를 고려하여 사전에 여유있는 할당을 시전하고 파편화 등을 방지할 수 있다.

24. 노는 SCV가 없게 하라.

= 멀티쓰레드 처리에 있어서 멍청한 join 대기 대신 유휴 쓰레드를 신속히 투입한다.

25. 스택에서 바로 할당

= 적은 동적 할당에 있어서는 alloca 등을 고려할 수 있다.

26. 벡터와 트리의 트레이드 오프.

= 기계의 뺑뺑이 능력을 최대한 뽑을 것인가, 트리의 지능적인 비용 단축을 노릴 것인가 계속 고민한다. 때로는 트리에도 벡터 인덱스를 따로 붙여둘 수 있다.

27. 비대칭에 대한 고려

= 쓰기만 있는지, 읽기가 압도적으로 많은지 작업에 따라 서로 다른 설계를 고안한다.

28. 스윗치의 해쉬 전환

= 대부분의 스윗치 분기는 인덱스 테이블에 의한 해시 O(1)으로 대체할 수 있다. 혹은 지루한 if 체인도 마찬가지. 조건부 함수 호출도 해시로 커버할 수 있다.

29. goto에 의한 상태머신 최적화

= 잘쓴 goto에 의한 상태머신 최적화는 더 빠르고, 읽기 쉬운 코드를 만든다. 예) 스윗치, while에 의한 뺑뺑이 대신 goto로 더 쉽게 코드를 짤 수 있는 경우도 있다.

30. goto에 의한 빠른 탈출

= goto에 의해 더 읽기 쉽고 빠른 코드 블럭 탈출을 고려할 수 있다.

31. 프로파일링

= 툴에 의존할 수도 있으나 습관적으로 직접 코드 구간 성능을 측정해본다. 프로파일링에 의한 측정은 초기 셋팅 비용이 크다.

32. libevent 등의 메커니즘을 잘 활용한다.

= 모든 유사 폴링은 잘 구현되어 있는 구조를 활용한다.

33. try, test, set 류의 탐구

= atomic, 메모리 배리어 류의 저수준 명령어들을 탐구하여 적절히 쓴다. 스핀락이나 쓰레드 조건 변수들을 직접 만들어 쓰기도 한다.

34. CPU 가용률 탐구

= 통으로 뽑아 먹어도 되는지, 같은 세입자의 민폐를 방지해야 하는지 고려한다.

35. nil의 활용

= 많은 자료구조에서 null 검사대신 nil 방식으로 널 검사와 값 추출을 동시에 할 수 있다.

36. 접근 데이터 처리 청크 단위 고민.

= 데이터를 벌크로 가져올 것인가 건 바이 건으로 가져올 것인가 고민한다.

37. 파이프 방식 처리

= 데이터나 업무의 처리를 체인화/파이프화 하여 모든 스테이지가 동시에 일어날 수 있도록 고려해 볼 수 있다.

38. 자체 캐시의 활용

= 레디스 등을 의존하지 않고 자체적인 데이터 재사용, 상태를 저장하는 DP식 패턴을 활용해 볼 수 있다.

39. 가용 레지스터 총동원

= 디스어셈블을 통해 가용 레지스터를 총동원 하여 쓰고 있는지, 씰데없이 스택에 보관했다 도로 꺼내쓰는지 확인해 볼 수 있다.

40. 고성능 필요 함수에 대한 아키텍쳐 특성 확인

= 각종 문자열, 메모리, 할당 함수들의 저수준에 아키텍쳐를 고려하여 고성능화를 구축해뒀는지 확인한다. 더불어 MT-Safe인지도 확인하고, 함수가 제멋대로 할당한 메모리를 수동으로 해제가 필요한지 체크한다. 랜덤류 처럼 함수 내부에 블럭킹이 있는지도 확인해가며 일한다.

41. 시스템 콜 확인

= 표준 함수의 기저에 시스템 콜 호출이 있는지 확인한다.

42. 표준 함수 후킹

= 표준 함수의 인과 아웃에 후킹 함수를 붙여 프로파일링 또는 본인의 개조부, 탐침등을 삽입한다.

43. 로딩 트레이드 오프

= 빠른 자원 로딩이 필요한 경우 동적/정적 라이브러리, fpic, 아예 컴파일 타임의 .data 임베딩등을 고민해 볼 수 있으며 컴파일 타임시 ld 적재 구조에 따른 성능 트레이드 오프를 고려할 수 있다. lazy 로딩이냐 즉시 로딩이냐를 결정할 수 있으며 ldd 등으로 로딩 순서를 보거나 시퀀스를 설계 할 수 있다.

44. directio냐, buffered io냐, mmap이냐, read/write냐 ioctl이냐.

= 디바이스 드라이버/파일 IO등에 있어서 어느 것을 쓸 지 고민할 수 있다. 표준 함수의 fread류는 충분히 버퍼링 고성능을 제공한다. 시스템 콜의 read류는 사용자가 잘 짜지 않으면 버퍼링/벌크/DMA 에 있어서 더 느리다. 페리가 느리면 directio는 더 느리지만 페리가 고성능이면 directio도 고려할 수 있다.

45. COW

= 쓰기 작업, 클렌징 작업이 실제로 언제 발생하는지 알고 있으며 이를 응용한 패턴을 짤 수 있다.

46. 응답 우선 순위에 따른 패턴 설계

= 응답 우선 순위에 따라 각각 용처에 맞는 코드를 고민한다. 인터럽트냐 폴링이냐 select냐 libevent냐 lazy loading이냐, COW 처럼 최후에 evaluation을 할 것인가.

47. 포킹 비용, vfork 메커니즘 등에 대한 이해.

= 포킹을 하더라도 공용부를 같이 쓸 수 있다. 쓰레드 모델이냐 프로세스 모델이냐 고민 할 수 있다.

49. IPC에 대한 이해

= 멀 쓸지 고민할 수 있다.

50. 기왕 잘난척하려고 100개는 채울 수 있을 줄 알았는데, 50개 채우기 더럽게 어렵구나.. 아직 멀었다..









[t:/] is not "technology - root". dawnsea, rss