Microsoft MVP성태의 닷넷 이야기
VC++: 62. 배열 초기화를 위한 기계어 코드 확인 [링크 복사], [링크+제목 복사],
조회: 22964
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

Visual C++ 배열 초기화를 위한 기계어 코드 확인

재미있는 논의가 있었군요. ^^

char* 문자열 버퍼 초기화의 내부
; http://www.gamedevforever.com/188

내용은 대충 이렇습니다. 다음과 같이 스택상에 생성되는 배열을 정의한 경우,

char temp[10] = { 0 }; // 1번
char temp[10] = { 1, 2, }; // 2번

1번의 경우에는 배열 요소가 모두 0으로 초기화 되는 반면, 2번의 경우에는 처음 두 번째 배열 요소만 각각 1, 2로 초기화 되고 이후의 경우에는 0으로 초기화 된다는 것입니다.

그런데, 이 초기화에 대한 방법이 본문과 덧글에서 다양하게 설명이 되고 있는데요.

  1. memset으로 초기화 된다.
  2. { , , } 안에 들어가는 숫자는 0으로 초기화 후에 해당 숫자로 셋팅되는 듯 하다.
  3. temp[0] = 1, temp[1] = 2가 먼저 이뤄지고 temp[2] ~ temp[9]까지는 4byte씩 0으로 초기화 된다.
  4. char temp[10] = {0, }; 컴파일 후 memset을 호출하는 이유는 컴파일러가 컴파일하면서 코드를 바꾸는 듯.

위의 4가지 설명은 아예 틀리는 것도 있고 절반만 맞는 경우도 있는데... 어쨌든 정확한 답은 아닙니다. 어디... 직접 확인을 해볼까요?




이런 의문을 해결하는 가장 좋은 방법은 Visual Studio의 디버그 모드에서 "Go to Disassembly..." 메뉴를 이용하여 어셈블리 코드 창을 함께 띄워놓고 보는 것입니다.

cpp_array_int_1.png

그럼, 답이 나올 텐데요. 문제는, 이것이 상황마다 초기화 방법이 틀리다는 것입니다.

우선, "char temp[3] = { 0 };" 코드에서부터 시작해 볼텐데요. 이는 Visual C++ 컴파일러에 의해서 다음과 같은 어셈블리 코드로 변환되는데,

mov         byte ptr [ebp-18h],0  
xor         eax,eax  
mov         word ptr [ebp-17h],ax 

C++ 코드로 정리해 보면 다음과 같은 효과를 갖습니다.

temp[0] = 0;
*(short *)&temp[1] = 0;

그렇다면 혹시 짝수로 4개의 char 요소를 가지면 어떻게 초기화 될지 예상이 되시나요?

    char temp[4] = { 0 };
mov         byte ptr [ebp-18h],0  
xor         eax,eax  
mov         word ptr [ebp-17h],ax  
mov         byte ptr [ebp-15h],al  

역시 C++ 코드로 정리하면 대충 이런 형태입니다.

temp[0] = 0;
*(short *)&temp[1] = 0;
temp[3] = 0;

개인적으로는, int로 형변환해서 4바이트를 한꺼번에 초기화할 줄 알았는데 ^^ 예상이 틀렸습니다. 어쨌든 이런 식으로 배열 크기가 5바이트부터 초기화 되는 기계어 코드를 유사한 C++ 코드로 바꿔보면 다음과 같습니다.

    char temp[5] = { 0 };
==> C++ 코드
    temp[0] = 0;
    *(int *)&temp[1] = 0;

    char temp[6] = { 0 };
==> C++ 코드
    temp[0] = 0;
    *(int *)&temp[1] = 0;
    temp[5] = 0;

    char temp[7] = { 0 };
==> C++ 코드
    temp[0] = 0;
    *(int *)&temp[1] = 0;
    *(short *)&temp[5] = 0;

    char temp[8] = { 0 };
==> C++ 코드
    temp[0] = 0;
    *(int *)&temp[1] = 0;
    *(short *)&temp[5] = 0;
    temp[7] = 0;

    char temp[9] = { 0 };
==> C++ 코드
    temp[0] = 0;
    *(int *)&temp[1] = 0;
    *(int *)&temp[5] = 0;

위의 결과만 보면, 배열 초기화는 memset으로 된다는 말은 전혀 맞지 않는 것 같습니다. 그럴까요? 위의 변환 과정을 보고 눈치채실 분도 계시겠지만 배열이 커지는 경우 최대 4바이트씩 초기화되는 *(int*)&temp[...] = 0 코드가 계속 늘어나는 것이 분명 비효율적이라는 것을 알 수 있습니다. 그렇죠? ^^ 따라서 어느 배열 크기 이후로는 함수 호출로 대체될 텐데요. 개인적으로 테스트하기에는 배열의 크기가 40을 넘어서면서 그렇게 바뀌는 것을 확인할 수 있었습니다.

그런데, 이 결과가 또한 재미있습니다.

    char temp1[40] = { 0 };
mov         byte ptr [ebp-50h],0  
push        27h  
push        0  
lea         eax,[ebp-4Fh]  
push        eax  
call        @ILT+1045(_memset) (0DB141Ah)  
add         esp,0Ch 

어떠세요? 이에 대해 C++ 코드로 다시 변형해 보면, 다음과 같은 코드가 나옵니다.

    temp[0] = 0;
    memset(&temp[1], 0, 0x27);

아마도 (저를 포함해서) 대부분의 사람들이 단순하게 "memset(temp, 0, 0x28)"을 예상했을 텐데 ^^ 아쉽게도 Visual C++ 컴파일러는 위와 같이 이상하게 변환을 해주었습니다.




사실, 위의 결과도 경우에 따라 다시 달라지는데요. 예를 들어, char 형이 아니라 int 형 배열이라면 어떻게 될까요? int는 4바이트라서 DWORD 크기에 딱 맞기 때문에 다음과 같은 식으로 초기화가 이뤄지다가,

mov         dword ptr [ebp-70h],0  
xor         eax,eax  
mov         dword ptr [ebp-6Ch],eax  
...[이하 남은 배열 수만큼 반복]...

배열 크기가 12가 되는 순간부터, memset을 사용하는 코드로 바뀌었습니다.

    int temp1[12] = { 0 };
mov         dword ptr [ebp-74h],0  
push        2Ch  
push        0  
lea         eax,[ebp-70h]  
push        eax  
call        @ILT+1045(_memset) (0FE141Ah)  
add         esp,0Ch  
==> C++ 코드
    temp[0] = 0;
    memset(&temp[1], 0, 11 * sizeof(int));

char 배열에서처럼, int 배열에서도 역시 첫 번째 배열 요소에 대해서는 값이 직접 입력되고, 나머지 배열 요소들은 memset에 전달되어 초기화 되었습니다.




마지막으로, 일부 배열 요소가 초기화된 경우는 어떤 코드로 컴파일이 될까요?

이에 대해서는 전체적인 규칙은 위에서 설명했던 것과 유사하게 됩니다.

    char temp2[3] = { 1, 2, };
mov         byte ptr [ebp-68h],1  
mov         byte ptr [ebp-67h],2  
xor         eax,eax  
mov         byte ptr [ebp-66h],al  
==> C++ 코드
    temp[0] = 1;
    temp[1] = 2;
    temp[2] = 0;

    char temp[4] = { 1, 2, };
mov         byte ptr [ebp-70h],1  
mov         byte ptr [ebp-6Fh],2  
xor         eax,eax  
mov         word ptr [ebp-6Eh],ax  
==> C++ 코드
    temp[0] = 1;
    temp[1] = 2;
    *(short *)(&temp[2]) = 0;

재미있는 것은 memset으로 바뀌는 시점의 배열 크기인데요. 처음 2바이트가 의미있는 코드를 가지기 때문에 이후 반복되는 코드 변환에서 누락되므로 40이 아닌 43 크기를 가지는 순간부터 memset으로 변환된다는 점입니다.




이쯤에서 문제에 대한 결론을 내려 볼까요?

배열 요소의 초기화 지정은 일정 수의 크기 이내에서는 0으로 직접 초기화를 하다가, 그 이후부터는 memset으로 바뀐다.


'일정 수'라는 크기는 컴파일러마다 다를 수 있고, 심지어 컴파일러의 버전마다 내부적으로 정해진 규칙에 의해 바뀔 수 있으므로 그 정확한 숫자를 기억하는 것은 별로 의미가 없을 것입니다. (사실, 위의 결과는 64비트 버전에서는 또 다를 수 있습니다.)

개인적으로 위의 테스트를 진행하면서 예전에 써둔 다음의 글들이 생각났습니다.

WPF 이벤트에 속한 핸들러 확인
; https://www.sysnet.pe.kr/2/0/624

CLR JIT 컴파일러가 생성한 기계어 코드 확인하는 방법
; https://www.sysnet.pe.kr/2/0/975

위에서도 확인되지만, 마이크로소프트는 '최적화'에 대해서 꽤나 신경을 쓰는 편이고... 사실 그것을 외부에서 정확히 '수치'적으로 정의하는 것은 다소 위험할 수 있습니다.

그러고 보니... 아래의 글까지 생각나는군요. ^^;

VC++에서 bool이 가지는 의미.
; https://www.sysnet.pe.kr/2/0/487








[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 6/26/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2012-06-03 04시04분
Clang 소스코드로 살펴보는 배열 초기화 코드
; http://minjang.egloos.com/2934460
정성태
2012-08-24 02시08분
[온천거북] 잘봤습니다.
[guest]

... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
12972정성태2/16/20228496Windows: 200. Intel CPU의 내장 그래픽 GPU가 작업 관리자에 없다면? [4]
12971정성태2/15/202210134.NET Framework: 1157. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 muxing.c 예제 포팅 [7]파일 다운로드2
12970정성태2/15/20228247.NET Framework: 1156. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 h264 형식의 파일로 쓰기 [1]파일 다운로드1
12969정성태2/14/20226701개발 환경 구성: 638. Visual Studio의 Connection Manager 기능(Remote SSH 관리)을 위한 명령행 도구 - 두 번째 이야기파일 다운로드1
12968정성태2/14/20226941오류 유형: 794. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for '...'.
12967정성태2/14/20227317VC++: 153. Visual C++ - C99 표준의 Compund Literals 빌드 방법 [4]
12966정성태2/13/20227149.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기파일 다운로드1
12965정성태2/13/20227153.NET Framework: 1154. "Hanja Hangul Project v1.01 (파이썬)"의 C# 버전
12964정성태2/11/20227459.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅파일 다운로드1
12963정성태2/11/20228200.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)파일 다운로드1
12962정성태2/9/20228039오류 유형: 793. 마이크로소프트 스토어 - 제품이 존재하지 않습니다. 재고가 없는 것일 수 있습니다.
12961정성태2/8/20228190.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c) [7]파일 다운로드1
12960정성태2/8/20227609개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
12959정성태2/7/20228308.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기 [2]파일 다운로드1
12958정성태2/6/20228342.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩 [2]파일 다운로드1
12957정성태2/6/20227930개발 환경 구성: 636. ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? [2]
12956정성태2/4/20227161.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [2]파일 다운로드1
12955정성태2/4/20226631개발 환경 구성: 635. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법 (2)
12954정성태2/4/20226476VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/20226671Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/20227203.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/20227142.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/20226697.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20226568.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20226704.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20227861개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...