C코드 최적화 - 전역변수 대신 지역변수 사용

반복문 등에서 반복적으로 사용되는 변수 등은 전역변수 말고 지연변수를 사용하는 게 빠르다. 지역변수는 CPU 레지스터에서 바로 사용되나, 전역변수는 RAM에서 레지스터로 읽어와서 결과를 쓰기 때문이다.
레지스터
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 어셈블리 명령어 설명

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 명령어를 사용하므로 더 성능이 높다.

* Feedback은 언제나 환영합니다.

Comments