C코드 최적화 - 전역변수 대신 지역변수 사용
반복문 등에서 반복적으로 사용되는 변수 등은 전역변수 말고 지연변수를 사용하는 게 빠르다. 지역변수는 CPU 레지스터에서 바로 사용되나, 전역변수는 RAM에서 레지스터로 읽어와서 결과를 쓰기 때문이다.
『레지스터
32bit CPU인 경우에, CPU에는 여러 연산(더하기, 빼기 등)을 처리할 수 있는 연산기가 있고, 내부에는 32bit 레지스터(register)가 여러 개 있다. 레지스터는 CPU가 사용하는 조그마한 저장 공간이다. CPU는 레지스터 저장공간을 사용해서 연산을 한다. 예컨대, c = a + b; 라면 CPU는 a, b를 각각 레지스터0, 레지스터1에 올려 놓고 더하기 연산을 해서 레지스터2에 더하기 결과를 넣을 수 있다.』
32bit CPU인 경우에, CPU에는 여러 연산(더하기, 빼기 등)을 처리할 수 있는 연산기가 있고, 내부에는 32bit 레지스터(register)가 여러 개 있다. 레지스터는 CPU가 사용하는 조그마한 저장 공간이다. CPU는 레지스터 저장공간을 사용해서 연산을 한다. 예컨대, c = a + b; 라면 CPU는 a, b를 각각 레지스터0, 레지스터1에 올려 놓고 더하기 연산을 해서 레지스터2에 더하기 결과를 넣을 수 있다.』
int global_sum = 0;
int calculate_sum_by_global_variable(void)
{
for (int i = 0; i < 10; i++)
{
global_sum += get_some_value(i);
}
}
void calculate_sum_by_local_variable(void)
{
int local_sum = 0;
for (int i = 0; i < 10; i++)
{
local_sum += get_some_value(i);
}
global_sum = local_sum;
}
위의 C코드 예제를 보면 지연변수를 사용하는 게 더 느릴 거라고 생각할 수도 있다.
C코드상으로는,
- 전역변수 사용 시에는 전역변수에 바로 합계를 계산하는 반면에,
- 지역변수를 사용 시에는 지역변수를 선언하고 합계를 계산한 후에 전역변수에 대입하기 때문이다.
하지만 CPU가 동작할 때는,
- 지역변수 사용 시에는 [①레지스터에 할당된 지역변수에 더하기]를 10번 한 후에 [②전역 변수(RAM)에 대입]하는 반면에,
- 전역변수 사용 시에는 [①전역변수를 RAM에서 읽어서 레지스터에 넣고 ②더하기를 한 후에 ③레지스터를 전역변수(RAM)에 넣는 것]을 10번 반복한다.
지역변수를 사용할 때보다 전역변수를 사용할 때 CPU에서 하는 동작이 많고, 더군다나 RAM 접근은 레지스터 접근보다 많이 느리기 때문에 전역변수를 사용하는 게 성능이 더 높다.
어셈블리 비교
* target CPU는 RISC-V이며 컴파일 조건은 아래와 같다.
포스팅 ▶ 테스트 조건 및 성능 / 메모리 사용량 비교 방법
* RISC-V 명령어 설명은 아래를 참조하자.
포스팅 ▶ RISC-V 어셈블리 명령어 설명
포스팅 ▶ 테스트 조건 및 성능 / 메모리 사용량 비교 방법
* RISC-V 명령어 설명은 아래를 참조하자.
포스팅 ▶ RISC-V 어셈블리 명령어 설명
00020000 [calcurate_sum_by_global_variable]:
# 함수 내에서 사용되는 레지스터를 스택에 백업
20000: ff010113 addi sp,sp,-16
20004: 00812423 sw s0,8(sp)
20008: 00912223 sw s1,4(sp)
2000c: 01212023 sw s2,0(sp)
20010: 00112623 sw ra,12(sp)
20014: 00000413 li s0,0 # s0=0 : int i = 0;
20018: 100004b7 lui s1,0x10000 # a1=0x10000000[global_sum]
2001c: 00a00913 li s2,10 # s2=10
20020: 00040513 mv a0,s0 # a0=s0 : a0 = i;
20024: fdddf0ef jal ra,0 [get_some_value] # a0 = get_some_value(a0);
20028: 0004a783 lw a5,0(s1) # a5 = *s1 : a5 = global_sum;
2002c: 00140413 addi s0,s0,1 # s0=s0+1 : i++;
20030: 00a787b3 add a5,a5,a0 # a5=a5+a0
20034: 00f4a023 sw a5,0(s1) # *s1 = a5 : global_sum = a5;
20038: ff2414e3 bne s0,s2,20020 # if(s1!=s2) jump to 20020
# 함수 내에서 사용되는 레지스터를 스택에 백업했던 것을 스택으로부터 레지스터로 복구
2003c: 00c12083 lw ra,12(sp)
20040: 00812403 lw s0,8(sp)
20044: 00412483 lw s1,4(sp)
20048: 00012903 lw s2,0(sp)
2004c: 01010113 addi sp,sp,16
20050: 00008067 ret # 함수리턴
00020054 [calcurate_sum_by_local_variable]:
# 함수 내에서 사용되는 레지스터를 스택에 백업
20054: ff010113 addi sp,sp,-16
20058: 00812423 sw s0,8(sp)
2005c: 00912223 sw s1,4(sp)
20060: 01212023 sw s2,0(sp)
20064: 00112623 sw ra,12(sp)
20068: 00000413 li s0,0 # s0=0 : int i = 0;
2006c: 00000493 li s1,0 # s1=0 : int local_sum = 0;
20070: 00a00913 li s2,10 # s2=10
20074: 00040513 mv a0,s0 # a0=s0 : a0 = i;
20078: f89df0ef jal ra,0 [get_some_value] # a0 = get_some_value(a0);
2007c: 00140413 addi s0,s0,1 # s0=s0+1 : i++;
20080: 00a484b3 add s1,s1,a0 # s1=s1+a0 : local_sum += a0;
20084: ff2418e3 bne s0,s2,20074 # if(s1!=S2) jump to 20074
# 함수 내에서 사용되는 레지스터를 스택에 백업했던 것을 스택으로부터 레지스터로 복구
20088: 00c12083 lw ra,12(sp)
2008c: 00812403 lw s0,8(sp)
20090: 100007b7 lui a5,0x10000 # a5=0x10000000[global_sum]
20094: 0097a023 sw s1,0(a5) # *a5 = s1 : global_sum = local_sum;
20098: 00012903 lw s2,0(sp)
2009c: 00412483 lw s1,4(sp)
200a0: 01010113 addi sp,sp,16
200a4: 00008067 ret # 함수리턴
- 전역변수 사용시에는 빨간색 어셈블리 명령어 5개를 10번 반복해서 수행하고, (5×10 = 50개 명령어)
- 지역변수 사용시에는 빨간색 어셈블리 명령어 3개를 10번 반복해서 수행하고, 파란색 명령어 2개를 수행한다. (3×10 + 2 = 32개 명령어)
위와 같이 지역변수 사용시에, 전역변수 사용 대비해서 더 적은 CPU 명령어를 사용하므로 더 성능이 높다.
Comments
Post a Comment