https://copynull.tistory.com/112?category=194491
변수에 쓸 데이터형, 한번쯤은 고려해보자
C 프로그래머의 입장에서 이야기를 시작하는 것이긴 하지만, 어떤 프로그래밍 언어일지라도 변수를 위한 다양한 데이터형이 존재한다. C에서는 char, short (혹은 short int), int (혹은 long int), double, float 등등이 존재하고, 자바에서도 C++을 모델로 삼았기 때문에 byte, int, float, double 등이 존재한다.
어떤 언어로든 프로그래밍을 하다보면, 구현하고자 하는 내용을 위해서 변수를 쓰게 된다. 데이터의 양에 따라서 그 크기를 결정하기도 하고, 혹은 저장해야하는 값의 범위나 종류에 따라서 데이터형을 결정하기도 한다. 그런데 작은 크기의 데이터를 저장할 변수는 일반적으로 int 보다는 char, 혹은 short int (혹은 WORD라고 표현하기도 한다) 등을 선호하는 편인 것 같다. 1, 2... 혹은 100, 200... 기껏해야 1000, 2000... 이러한 값을 저장하기 위한 변수를 굳이 몇억이라는 숫자를 표현가능한 변수를 사용하지 않아도 될법하기 때문이다.
하지만 그것이 과연 옳은 선택인가에 대하여 한번쯤은 생각해볼 필요가 있다.
CPU는 word 단위로 연산한다
프로그래밍에 대하여 공부하는 사람이라면, 정말 초보가 아닌 이상에는, CPU의 word라는 개념을 들어봤을 것이다. 이것은 위에서 말한 WORD(short int)를 말하는 것은 아니고, CPU가 한번에 처리할 수 있는 데이터의 크기를 의미한다. 그리고 동시에 CPU내에 있는 레지스터의 크기이기도 하다.
혹시나 레지스터를 모르는 사람을 위해 잠시 언급하자면, CPU가 동작(일반적으로 연산이라고 한다)할때 사용하는, CPU내의 작고 빠른 기억장치 정도로 생각하면 되겠다. 속도도 속도지만, CPU의 연산장치는 메모리를 직접 액세스하지 않기 때문에 CPU내의 레지스터로 값을 읽어온 다음에 연산처리를 하게 된다. 예를 들면, 메모리에 있는 a 변수와 b 변수의 값을 서로 더하려고 할때, b 값을 a에다 더하는게 아니라, a와 b를 레지스터에 복사한뒤, 두 레지스터끼리 덧셈을 하고, 다시 최종적으로 메모리에 있는 변수에다 넣어주는 셈인 것이다.
다시 앞으로 돌아가서, CPU는 계산할때에 word 크기인 레지스터를 사용하게 되므로 word와 다른 크기의 변수를 사용할 때에는 부수적인 동작(이하 오버헤드.overhead)가 필요하게 된다. 그 오버헤드로 인하여 오히려 코드의 복잡도가 증가하고, 코드크기의 증가 혹은 실행속도지연 등으로 이어지게 된다면, 작은 크기의 데이터형을 쓰는 것이 좋은 선택인것만은 아니라는 것이다.
크기가 작은 데이터형의 오버헤드
간단한 C 소스를 작성하고 컴파일했을때의 어셈블리어를 확인해보고, 데이터형에 따라 실제코드가 어떻게 변하는지를 살펴보자.
여기서 한가지 전제가 필요한데, 어셈블리어는 시스템마다 다르기 때문에 가급적 기본적인 어셈블리어를 사용하도록 한다.
자 그러면, C 소스로 65 라는 값을 데이터형이 다른 각각의 변수에 넣어보자.
char a = 65;
short b = 65;
int c = 65;
이때 각 변수들의 데이터크기는 어떻게 될까?
물론 a는 1 byte, b는 2 bytes, c는 4 bytes일 것이다.
그런데 실제 연산할때에는 다들 4 bytes크기의 CPU 레지스터를 사용한다. CPU는 실행코드가 있는 RAM에서 직접 연산을 할 수 없기 때문에, 모든 연산을 할때에는 레지스터에 값을 가져와서 계산을 하기 때문이다. 우리가 일반적으로 사용하는 32 bits 컴퓨터는 레지스터 크기가 4 bytes이므로, 메모리에 저장되었을때에는 그보다 작은 크기의 메모리를 차지하겠지만, 계산시에는 아무런 이득이 없다.
자 그럼, 컴파일된 내용을 하나씩 어셈블리어로 살펴보자.
char a = 65;
mov r4, #0x41
short b = 65;
mov r5, #0x41
int c = 65;
mov r6, #0x41
간단하게 설명을 하자면, mov는 오른쪽의 값을 왼쪽으로 대입하는 명령이고, r4, r5, r6은 각각의 레지스터를 의미한다. 그리고 #0x41은 우리가 입력하고자 한 65의 16진수 값이다.
여기선 그다지 별다른 건 없다.
그러면 이번엔 각 변수에다 여기다 1000 이란 값을 더해보자.
a = a + 1000;
b = b + 1000;
c = c + 1000;
a는 char 형이기 때문에 범위를 넘어선다. 그렇기 때문에 1065가 아닌 다른 값이 들어갈테고, b와 c는 1065란 값이 들어갈 것이다.
그럼, 어셈블리어를 한번 보자.
a = a + 1000;
add r0, r4, #0x3E8
and r4, r0, #0xFF
b = b + 1000;
add r5, r5, #0x3E8
c = c + 1000;
add r6, r6, #0x3E8
a 변수를 계산하는 부분에서는 다른 부분과 달리 'and r4, r0, #0xFF'가 추가되었다. 이것이 무엇을 의미하는지 눈치챈 사람도 있겠지만, 간단하게 적어보겠다.
r4와 1000 (0x3E8)을 더해서 r0에 넣었다면 1065가 되었을 것이다. 하지만, a 변수는 char형, 즉 1 byte 크기이기 때문에 범위를 넘어서는 데이터를 잘라내기 위해서 0xFF와 and 연산을 하는 것이다. 풀어서 계산하면, ( 0x41 + 0x3E8 ) & 0xFF = 0x429 & 0xFF = 0x29 = 41 이 되는 것이다.
여기서 이러한 의문을 가지는 사람도 있을 것이다. 프로그래밍할 때에는 값이 작을때만 작은 변수인 char를 쓰니까 저런 추가코드가 필요없지 않느냐라고 말이다. 만약 이렇게 생각했다면, 한가지 간과한 점이 있다. 바로 프로그래밍을 할 때 입력하는 대부분의 연산은 상수를 직접 입력하는게 아니라 변수끼리의 연산을 한다는 점이다.
b = b + 1000;
이것을 어셈블리어로 번역하게 될때에는 b는 반드시 65이고, 여기서 1000 을 더하더라도 b가 표현가능한 2 bytes 범위를 넘지 않기 때문에 어셈블리어 코드가 없는 것이다. 즉, 상수가 아니라 변수를 사용하는 코드라면 변수의 크기만큼 데이터를 잘라내는 코드가 반드시 들어가게 된다는 말이다.
하지만, 아래의 코드를 보자. 일반적인 함수형태의 코드이다.
short add_1000(short e)
{
e = e + 1000;
return e;
}
사실 이러한 형태와 같이 계산하는 값이 정해지지 않은 경우가 더 많다. 그러므로 e의 값에 1000을 더했을때, short가 표현할 수 있는 -32768 ~ 32767 값을 벗어날지 아닐지는 모른다. 하지만 연산을 할때에는 레지스터를 사용하게 되고, 그 레지스터들은 4 bytes 크기로 int 형과 동일하므로, short 의 범위값을 넘어서는 값도 저장이 가능하다. 그렇기 때문에 이러한 경우에는 4 bytes 중 short int 로서 유효한 2 bytes만을 잘라주는 코드가 컴파일시에 추가된다.
short 형의 경우에는 char형과 같이 0xFFFF를 and 연산할 수도 있지만, bit shift 방법을 쓰기도 한다.
좌측으로 16 bites shift한 다음, 다시 우측으로 16 bits shift 하게 되면, 상위의 2 bytes에 저장된 값은 지워지고, 하위 2 bytes의 값만 남게 되는 식이다.
물론 컴파일러마다 다르기 때문에 항상 어떻게 구현된다라고 알려줄 수는 없지만, 어찌되었든 중요한 것은 크기가 작은 데이터형의 변수를 사용할때에는 그 범위를 제한하기 위하여 추가적인 코드가 동반된다라는 점이다.
4 bytes짜리 int형 대신 char형 변수를 사용하여 3 bytes를 아끼고자 했지만, 실행코드가 4 bytes가 늘어나고 동작속도도 추가되는 실행코드 때문에 더 느려지고 있는 것도 모른채 혼자 뿌듯해하면 좀 곤란하다는 얘기다.
컴파일러는 컴파일시 optimization을 하기 때문에 그나마 중복되는 코드를 최소화 해주긴 한다. 하지만 당연히 줄여줄 수 없는 부분은 못줄여준다. 교묘하게 메모리 더 많이 차지하게 짜놓은 불량 코드는 컴파일러의 최적화 기능도 무용지물일 수 있다.
그리고, 혹시 작성코드를 어셈블리어로 확인하고자 할때에 Visual Studio 같은 툴을 이용하면 고급언어 스타일의 어셈블리어로 표현하기 때문에, 위에서 언급한 내용을 제대로 확인하지 못할 수도 있다는 점을 참고바란다.
최종적인 결정은 자신이 해야한다. 그러나...
char 대신에 int를 사용한다면 속도저하는 줄어들지 모르나, 그만큼 메모리를 많이 소모하게 될 것이다. 이것은 프로그래머 자신이 선택해야할 문제이다. 데이터가 많은지 아니면 액세스 반복이 많은지에 따라서 결정하면 적어도 무난한 선택이 될 것이라 믿는다.
내가 말하고 싶은건, 이런 방법을 쓰던지 저런 방법을 쓰던지간에, 그래도 알고는 있자는 것이다.
"숫자 범위가 넘지만 않는다면, 당연히 int 대신에 char을 쓰는게 무조건 100% 옳죠!"
이런 말을 하는 경우는 없도록 말이다. <끝>
위 글을 읽고 32비트와 64비트에서 각각 코드를 작성하여 테스트 해보았습니다, 결과는 32비트에서는 short보다 int가 더 빠르다고 나왔습니다
64비트에서 똑같이 구동한 결과 short이 int보다 더 빠르게 나타났습니다. 아마도 cpu 구조적인 부분이 성능에 미치는 결과가 작용한거 같다고 생각됩니다. 즉, cpu 코어의 수, 캐시, 레지스터 등 하드웨어에 따른 vs에서의 최적화에 따라서 결과가 달라지는것으로 보여졌습니다.
아래는 코드 구동 시간 결과
<32비트 short, int 구동 결과>
<64비트 short, int 구동 결과>