Type-Generic 함수 만들기, 정적 어서트, 메모리 정렬, 멀티스레딩
<tgmath.h>와 제네릭 선택
- <tgmath.h>
- 매개변수 형에 알맞는 수학 함수를 찾아서 호출해주는 매크로 함수
- 컴파일러가 알아서 구현해 준 것
- 프로그래머가 이런 매크로를 직접 만들 방법이 없었음
- C11에서는 가능
- 제네릭 선택(generic selection)이라 부룸
- _Generic 키워드를 사용
- 이제 <tgmath.h>도 이 키워드를 사용해서 직접 구현 가능
_Generic 키워드
_Generic(<제어 표현식>, <연관 목록>)
- 컴파일 도중에 여러가지 표현식 중 하나를 선택하는 방법
- 실행 중에 선택하는 것이 아님
- 매크로 함수의 대체 목록으로 사용하는 게 일반적
- 흡사 switch 문과 비슷해 보임
_Generic으로 ceil() 함수를 구현한 예
#include <stdio.h>
#include <math.h>
#define ceil(X) _Generic((x), \
long double: ceill, \
default: ceil, \
float: ceilf)(X)
int main(void)
{
float num1 = 3.1415;
double num2 = 69798.8979;
printf("ceil(%f) = %f\n", num1, ceil(num1));
printf("ceil(%f) = %f\n", num2, ceil(num1));
}
연관 목록
_Generic(<제어 표현식>, <연관 목록>)
-
연관 목록의 각 항목은 다음의 형태를 가짐
<자료형>: <호출할 함수이름> default: <호출할 함수이름>
-
대체 규칙: 제어 표현식의 형에 따라 다음과 같이 대체
- 연관 목록에 그 형이 있으면 그에 대응하는 표현식으로 대체
- 연관 목록에 형이 없고 default: 가 있다면 그에 대응하는 표현식으로 대체
- default: 도 없다면 컴파일 되지 않음
어서트는 프로그래머의 베프
- 코드 작성 시 프로그래머가 세운 가정 또는 선조건이 올바른지 검사
- 개발 중에 실수를 일찍 발견할 수 있게 도와줌
- 예: 구조체의 크기 보장하기
typedef struct status{
unsigned int level;
unsigned int exp;
} status_t;
assert(sizeof(status_t) == 8);
assert(sizeof(int) == 4);
어서트의 한계 - 플랫폼마다 특정 타입의 크기가 달라짐
아예 컴파일이 안 되게 하자!
- 어떤 가정(ex. int 크기는 4)이 깨질 경우 아예 컴파일이 안 되면 됨
- 그러려면 어서트 조건을 컴파일 중에 평가할 수 있어야 함
- 하지만 실행 중에만 판단 가능한 조건들은 이런 게 불가능
- 컴파일 중에 판단 가능한 조건들도 충분히 많다
- 구조체 및 기본형의 크기 등
- 이런 조건이 충족 안 할 때 컴파일을 막아주는 것이 정적 어서트
정적 어서트
_Static_assert(<표현식>, <메시지>)
- <assert.h>를 인클루드 하면 static_assert가 정의되어 있음
#define static_assert _Static_assert
_Static_assert(sizeof(status_t) == 8, "status_t size mismatch");
// 혹은
static_assert(sizeof(status_t) == 8, "status_t size mismatch");
- <표현식>이 0으로 평가되면 컴파일 오류
- 무슨 문제인지 설명해주는 메시지도 '<메시지>'에 넣을 수 있음
- 그런데 이건 안 됨...
static_assert(sizeof(status_t) == 8);
- 추후 C 표준에서 어서트 조건만 받는 버전이 들어온다 함
베스트 프랙티스 : 어서트 vs 정적 어서트
- 컴파일 중에 평가될 수 있는 조건이라면 무조건 정적 어서트
- 컴파일이 안 되면 실행파일 자체가 안 나옴
- 따라서 프로그래머가 어떻게든 고침
- 단, C11에서만 사용 가능
- 그 외의 조건은 어서트를 사용할 것
- C11을 지원하지 않는 컴파일러를 사용한다면?
- static_assert를 assert로 #define할 것
- C11에서는 정적 어서트로 동작
- 그 전 표준에서는 동적 어서트로 동작
_Noreturn 키워드
_Noreturn <반환형> <함수이름> (<매개변수>)
- <stdnoreturn.h>를 인클루두 하면 noreturn을 대신 사용 가능
noreturn void cross_the_river()
{
// 코드 생략
}
- void는 이미 아무것도 반환하지 않겠다는 의미?
- void는 반환하는 값이 없다는 의미
- 여전힘 함수에서 원래 호출자로 돌아오긴(return) 함
- noreturn은 그 함수에서 원래 호출자로 돌아가지 않는다는 의미
- 멀티 스레딩을 할 때 필요한 경우 있음
- 어떤 함수에서 프로그램을 종료시켜 버리는 경우도 해당
표준 라이브러리의 _Noreturn 함수들
- 다음의 표준 라이브러리 함수들은 절대 호출자로 돌아오지 않음
- abort()
- exit()
- _Exit()
- quick_exit()
- thrd_exit()
- longjmp()
- 대부분 무언가를 종료하는 함수들
베스트 프랙티스 : 별로 안 써요
- 어짜피 이런 함수는 많지 않음
- 따라서 noreturn을 쓸 곳이 별로 없을 것임
- 그래서 최적화 때문에 noreturn을 붙인다는 것도 좀 오버
- 그 대신 자체 문서화로서의 역할이 더 강하다고 볼 수 있음
- 프로그래머가 함수 시그니처를 딱 보는 순간 판단할 수 있음
- "이 함수는 절대 반환되지 않는구나!"
malloc()으로 할당한 메모리의 주소 예
int* int_p1 = malloc(sizeof(int));
char* char_p = malloc(sizeof(char));
int* inn_p2 = malloc(sizeof(int));
printf("int_p1: %p\n", (void*)int_p1);
printf("char_p: %p\n", (void*)char)p);
printf("int_p2: %p\n", (void*)int_p1);
free(int_p1);
free(char_p);
free(int_p2);
- 모두 4의 배수가 출력됨
왜 4의 배수일까?
- 표준은 이런 걸 정하지 않음
- 데스크탑에서 실행하면 이런 패턴을 볼 수 있을 뿐
- 이유?
- 32비트에서는 4바이트 단위로 메모리를 접근하는 게 성능상 유리
- 구조체에서 컴팡리러가 알아서 패딩을 붙이는 이유와도 비슷
- 이렇게 시작 주소가 4의 배수로 나눠 떨어지는 메모리를 4바이트로 정렬된(aligned) 메모리라고 함
프로그래머가 직접 메모리를 정렬할 일이 있을까?
- 하드웨어에 따라 직접 지정해줘야 하는 경우가 있음
- 특정 바이트로 정렬을 하면 성능 향상이 되는 경우
- 정렬을 안 해도 여전히 동작은 함
- 속도가 느려질 뿐
- 정렬을 하지 않으면 아예 동작하지 않는 경우
- 정렬되지 않은 메모리를 하드웨어가 처리할 수 없는 경우가 이에 해당
- 예: 그래픽 카드
- 이 경우는 프로그램 실행 중에 크래시가 나는 경우가 대부분
- 정렬되지 않은 메모리를 하드웨어가 처리할 수 없는 경우가 이에 해당
aligned_alloc()
void* aligned_alloc(size_t alignment, size_t size);
- alignment : 메모리 시작 주소가 정렬되어야 하는 바이트
- size : 할당할 바이트의 크기. 반드시 alignment의 배수여야 함
- 반환값
- 성공 시 : 할당된 메모리 주소
- 실패 시 : 널 포인터
- 실패 조건
- alignment가 구현에서 유효하지 않거나 지원하지 않는 크기일 때
- size가 alignment의 배수가 아닐 때
- 단, 첫 C11 버전에서는 널 포인터 반환이 아님(결과가 정의되지 않음)
- 수정 버전(DR 460)부터 널 포인터 반환
aligned_alloc() 예
int* p1 = aligned_alloc(4096, sizeof(int));
printf("p1: %p\n", (void*)p1);
// 단, 하나의 정수형을 할당하더라도 정렬 크기(4096)의 배수를 유지해야 함
int* p2 = aligned_alloc(4096, 4096 * sizeof(int));
printf("p2: %p\n", (void*)p2);
free(p2);
free(p1);
// 올림 함수
size_t aligned_up(const size_t alignment, const size_t size)
{
return (size + alignment -1) / alignment * alignment;
}
const size_t num_bytes = align_up(4096, sizeof(int));
int* p = aligned_alloc(4096, num_bytes);
printf("p : 0x%p\n", (void*)p);
free(p);
_Alignas 키워드
_Alignas(<표현식>)
- 스택 메모리에 생기는 변수들 정렬
- <stdalign.h>를 인클루드하면 alignas로 사용할 수 있음
int num1;
alignas(4096) int num2;
int num3;
printf("num1: %p\n", (void*)&num1);
printf("num2: %p\n", (void*)&num2);
printf("num3: %p\n", (void*)&num3);
구조체 정렬
- 두 가지 방법이 있음
- 각 멤버를 따로 정렬
- 모든 멤버를 일괄적으로 같은 크기로 정렬
- 구조체 변수를 선언할 때, 그 변수의 시작 주소도 정렬 가능
구조체 정렬 예1 : 멤버 변수 별 정렬
typedef struct data{
alignas(4096) int num; // int가 4바이트 차지하니, 실제로는 1024 차지
alignas(1024) int dummy[10]; // data 주소로부터 1024 띈 부분에 할당
} data_t;
data_t data = {0, };
printf("data size: %d, alignof: %d\n", sizeof(data), _Alignof(data));
printf("data.num: %p\n", (void*)&data.num);
printf("data.dummy: %p\n", (void*)&data.dummy);
구조체 정렬 예2 : 모든 멤버 변수 동일 정렬
typedef struct data{
int num;
int dummy[10];
} data_t;
typedef struct aligned_data{
int num; // 여기도 4096으로 잡힘
alignas(4096) int dummy[10];
} aligned_data_t;
구조체 변수 선언 시 메모리 정렬
typedef struct data {
int num;
int dummy[10];
} data_t;
alignas(4096) data_t data;
printf("data size: %d, alignof: %d\n", sizeof(data), _Alignof(data)); // 44, 4096
printf("data: %p\n", (void*)&data);
동적 할당을 할 경우엔 주의하자
- malloc() 함수는 구조체용 메모리를 할당한다는 사실을 모름
- 따라서 구조체 멤버변수의 정렬에 맞게 메모리 할당이 안 됨
- 그 대신 aligned_alloc()을 사용해야 함
- 메모리 크기도 반드시 배수가 되어야 한다는 점 잊지 말 것
- 아까 봤던 aligned_up() 함수 같은 걸 만들어서 쓸 것
typedef struct data {
int num;
alignas(4096) int dumy[10];
} data_t;
const size_t alignment = _Alignof(data_t);
const size_t size = align_up(alignment, sizeof(data_t));
data_t* p = aligned_alloc(alignment, size);
printf("data: %p\n", (void*)p);
free(p);
변수가 몇 바이트 정렬인지 아는 법
- 가장 간단한 방법은 나머지 연산을 하는 것
boos is_aligned(const void* const p, const size_t align)
{
return ((unsigned int)p % align) == 0;
}
_Alignof()
_Alignof(<자료형>)
- <stdalign.h>를 인클루드 하면 alignof()로 사용 가능
int num1;
alignas(4096) int num2;
int num3;
printf("align of num1 = %d\n", _Alignof(num1)); // 4
printf("align of num2 = %d\n", alignof(num2)); // 4096
printf("align of num3 = %d\n", alignof(num3)); // 4
멀티 스레딩
- C11 이전에는 멀티 스레딩을 지원하지 않음
- 언어 자체에서 지원하지 않음. 운영체제의 몫이었음
멀티 스레딩을 감독하는 건 운영체제
- 여러 스레드가 프로그램을 나눠 실행하려면 감독이 있어야 함
- 그 감독 겸 지휘자가 운영체제
C11 이전의 멀티 스레딩은 OS 함수를 사용
- 각 운영체제 별로 스레드를 관리하는 함수가 있었음
- C 프로그래머는 각 운영체제가 제공하는 함수들을 호출했을 뿐
- 즉, 어쩔 수 없이 포팅이 불가능한 코드를 작성
- 플랫폼마다 #ifdef를 써서 처리했던 게 일반적
C11 이전의 멀티 스레딩은 운영체제가 담당
- 그것을 하나로 합쳐서 C11 표준으로 만든 게 전부
- 따라서 이제 포팅이 가능한 코드 작성이 가능
멀티 스레드 환경에서 가장 큰 문제 중 하나
- 여러 스레드가 동일한 메모리에 접근하는 것(Race Condition)
- 해결법 : 어떤 스레드가 접근하는 동안 다른 스레드의 접근을 막음
- 이를 보통 락(lock)을 건다고 함
- 이 때 사용하는 대표적인 락의 종류가 바로 뮤텍스(mutex)
그러나 락을 잠그고 여는 행위는 너무 느리다
- 따라서, 많은 CPU는 락을 안 써도 되는 atomic 연산을 지원
- 락을 안 써도 멀티 스레드 환경에 안전
- 단, 기본 데이터 형이어야만 함(ex. int)
- 간단한 연산에 한함(ex. 변수값 1 증가하기)
atomic의 의미
- 어떤 연산을 더 이상 쪼갤 수 없는 하나의 단위로 보는 것
- 메모리 읽기(read) - 데이터 갱신(update) - 메모리 저장(store)
- 이 자체를 하나의 단위로 보겠다는 의미
- 즉, 이 자체가 하나의 연산이 됨
- atomic 연산 동안에는 다른 스레드가 개입할 여지가 없음
- 3단계 모델에서는 중간에 개입해서 서로 값을 덮어쓰는 경우가 생김
- C11은 CPU의 atomic 명령어들을 프로그래머에게 노출해줌
- 변수 선언 앞에 붙일 수 있는 _Atomic 키워드
- atomic 연산에 사용할 수 있는 함수들
_Atomic
_Atomic <자료형>
- STDC_NO_ATOMICS이 정의되어 있으면 지원 안 함
- 뒤에 볼 함수들도 마찬가지
- <stdatomic.h>에 키워드랑 형을 미리 합쳐 정의해좋은 것도 있음
- atomic_bool, atomic_char ... (총 37종)
기본형 변수를 atomic으로 선언하기
// _Atomic 키워드를 사용할 때
static _Atomic int s_num_threads;
static _Atomic _Bool s_exiting;
// 편의성 매크로를 사용할 때
static atomic_int s_num_threads;
static atomic_bool s_exiting;
- 이렇게 선언만 해놓으면 일반 변수 사용하듯이 사용하면 됨
- _Atomic을 안 붙이면 ++연산자를 사용하더라도 3단계로 동작
atomic 함수들
- 다음과 같은 함수들이 있음
- atomic_init()
- atomic_flag_test_and_set()
- atomic_load()
- atomic_store()
- ...
CPU가 atomic을 지원 안 할 경우
- 여전히 atomic 키워드와 함수를 사용할 수도 있음
- STCD_NO_ATOMICS이 정의되어 있지 않는 한
- C에서 내부적으로 락을 대신 사용할 수도 있음
- 그건 다음의 매크로들을 확인하면 됨
- ATOMIC_BOOL_LOCK_FREE
- ATOMIC_CHAR_LOCK_FREE
- ATOMIC_INT_LOCK_FREE
- ATOMIC_LLONG_LOCK_FREE
- etc.
- 0 : 지원 안 함, 1 : 지원하지만 가끔 atomic이 아님, 2 : 언제나 atomic
_Thread_local
_Thread_local <자료형>
- STCD_NO_THREADS이 정의되어 있으면 지원 안 함
- <threads.h>를 인클루드 하면 thread_local을 쓸 수 있음
- 줄여서 흔히 TLS(thread local storage)라고 부름
- atomic 변수와 마찬가지로 자료형 앞에 붙이는 키워드
- 그러면 각 스레드마다 이 변수의 사본이 생기는 꼴!
static _Thread_local int s_num;
static thread_local int s_num;
스레드 지원 라이브러리: <threads.h>
- 스레드 관리용 함수 및 자료형을 제공
- 선택적 구현 사항
- 스레드 관련 코드를 작성할 때, STDC_NO_THREADS를 확인할 것
스레드 관련 함수들
- thrd_create() : 스레드 만들기
- thrd_join() : 스레드 실행이 끝나는 것을 기다리기
- thrd_yield() / thrd_sleep() : 실행 순서 양보하기
- 뮤텍스와 조건 변수 이용하기
- mtx_init() / mtx_destory()
- mtx_lock() / mtx_trylock() / mtx_unlock()
- cnd_init() / cnd_destory()
- cnd_signal() / cnd_broadcast() / cnd_wait()
- etc...
'프로그래머 > C, C++' 카테고리의 다른 글
[HackerRank] Virtual Functions | 클래스 상속 | 가상함수 | 클래스 정적 멤버변수 (0) | 2021.01.27 |
---|---|
[포프 tv 복습] C99, C11 (0) | 2020.12.03 |
[포프 tv 복습] 나만의 라이브러리 만들기, C99 (0) | 2020.12.01 |
[포프 tv 복습] 전처리기 (0) | 2020.11.30 |
[면접 대비] C를 사용한 해시 맵 구현 (0) | 2020.11.29 |