Info

해당 글에서는 Native Type 보다 작은 타입이 들어올 경우 Promote가 생기는 과정에서 일어날 수 있는 비효율에 대해서 알아보고 이것이 유리할 수 있는 상황에 대해서 고민해본다.

우리는 ALU나 레지스터에서 기본적으로 사용되는 크기를 Native size라고 부른다. 이는 별도의 Promoting 없이 연산을 수행하게 되고, 이에 따라서 일반적으로 보다 작은 코드의 크기 (binary code size)와 빠른 연산 속도를 가지게 된다.

구체적으로는 다음의 코드를 살펴보자.

#include <stdint.h>
 
volatile int result_u8 = 0;
volatile int result_u32 = 0;
volatile uint32_t cycles_u8_func = 0;
volatile uint32_t cycles_u32_func = 0;
 
__attribute__((noinline)) // Prevent inlining
int test_uint8_version(void) {
    uint8_t a = 10;
    volatile uint8_t b = 100; // Read from "memory"
    uint8_t c = (a + b);
    return (c > 150);
}
 
__attribute__((noinline)) // Prevent inlining
int test_uint32_version(void) {
    uint32_t a = 10;
    volatile uint32_t b = 100; // Read from "memory"
    uint32_t c = (a + b);
    return (c > 150);
}
 
static inline void DWT_Init(void) {
    if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
        CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // Enable DWT and ITM
    }
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;   // Enable Cycle Counter
}
 
static inline void DWT_Reset(void) {
    DWT->CYCCNT = 0; // Reset Cycle Counter
}
 
static inline uint32_t DWT_GetCycles(void) {
    return DWT->CYCCNT;
}
 
/**
 * @brief  main function used for debug purpose
  * @retval the function always returns 0
 */
int main(void)
{
	DWT_Init(); // Initialize the DWT Cycle Counter
 
	// --- Benchmark uint8_t version ---
	DWT_Reset();
	result_u8 = test_uint8_version();
	cycles_u8_func = DWT_GetCycles();
 
	// --- Benchmark uint32_t version ---
	DWT_Reset();
	result_u32 = test_uint32_version();
	cycles_u32_func = DWT_GetCycles();
    while (1);
 
    return 0;
}

위 코드에 대응하는 어셈블리 코드는 다음과 같다.

          test_uint8_version:
341808ac:   movs    r3, #100        @ 0x64
341808ae:   sub     sp, #8
341808b0:   strb.w  r3, [sp, #7]
341808b4:   ldrb.w  r0, [sp, #7]
341808b8:   adds    r0, #10
341808ba:   uxtb    r0, r0
341808bc:   cmp     r0, #150        @ 0x96
341808be:   ite     ls
341808c0:   movls   r0, #0
341808c2:   movhi   r0, #1
341808c4:   add     sp, #8
341808c6:   bx      lr
          test_uint32_version:
341808c8:   movs    r3, #100        @ 0x64
341808ca:   sub     sp, #8
341808cc:   str     r3, [sp, #4]
341808ce:   ldr     r0, [sp, #4]
341808d0:   adds    r0, #10
341808d2:   cmp     r0, #150        @ 0x96
341808d4:   ite     ls
341808d6:   movls   r0, #0
341808d8:   movhi   r0, #1
341808da:   add     sp, #8
341808dc:   bx      lr
341808de:   nop     
          main:
341808e0:   push    {r3, lr}
341808e2:   ldr     r3, [pc, #68]   @ (0x34180928 <main+72>)
341808e4:   ldr.w   r2, [r3, #252]  @ 0xfc
341808e8:   lsls    r2, r2, #7
341808ea:   bmi.n   0x341808f8 <main+24>
341808ec:   ldr.w   r2, [r3, #252]  @ 0xfc
341808f0:   orr.w   r2, r2, #16777216       @ 0x1000000
341808f4:   str.w   r2, [r3, #252]  @ 0xfc
341808f8:   movs    r1, #0
341808fa:   ldr     r2, [pc, #48]   @ (0x3418092c <main+76>)
341808fc:   ldr     r4, [pc, #48]   @ (0x34180930 <main+80>)
341808fe:   ldr     r3, [r2, #0]
34180900:   orr.w   r3, r3, #1
34180904:   str     r3, [r2, #0]
34180906:   str     r1, [r2, #4]
34180908:   bl      0x341808ac <test_uint8_version>
3418090c:   str     r0, [r4, #0]
3418090e:   ldr     r0, [r2, #4]
34180910:   ldr     r3, [pc, #32]   @ (0x34180934 <main+84>)
34180912:   str     r0, [r3, #0]
34180914:   str     r1, [r2, #4]
34180916:   bl      0x341808c8 <test_uint32_version>
3418091a:   ldr     r1, [pc, #28]   @ (0x34180938 <main+88>)
3418091c:   ldr     r3, [pc, #28]   @ (0x3418093c <main+92>)
3418091e:   str     r0, [r1, #0]
34180920:   ldr     r2, [r2, #4]
34180922:   str     r2, [r3, #0]

조금 복잡해 보일 수 있는데, 함수 test_uint32_version과 test_uint8_version의 다른점만 살펴보면 다음의 명령어가 다른것을 알 수 있다.

341808ba:   uxtb    r0, r0

해당 명령은 Unsigned byte extend의 약자로 1 byte보다 상위의 bit를 0으로 clear하는 명령이다.
이제 다시 어셈블리 코드를 살펴보면 일반적인 연산을 수행한 이후에, uxtb를 통해서 값을 조정해주는 것을 볼 수 있다.


일반화하기는 어렵지만 오늘 내가 찾아본 바에 의하면 이러한 연산이

“모든 연산은 결국 int로 캐스팅되어 연산된다”

는 것의 실체인듯 하다.


이것이 만약 보수적으로 1~50 사이클 정도를 추가로 소비한다고 가정해봐도 이는 1~50 나노초 정도의 굉장히 미미한 성능을 발휘한다.

고작 이정도의 성능차이라면 개인적으로 ‘캐시라인’에 더 맞을 수 있도록 값들을 보다 촘촘하게 배치하는 것이 이득이라고 생각한다.

물론 조금의 시간도 아깝고 용납하기 힘든 경우에는 피하는 편이 좋을 것이라 생각되긴한다. 하지만 이외의 경우에는 동의하기 힘들듯 하다.

내 예상과 다른 결과라서 굉장히 당황스럽다. 일반적으로 통용되는 생각과는 다른 결과가 도출되니 조금은 불안함도 남는 결론인듯하다.