C 프로그램 성능 최적화 방법 (1)

5174 단어 프로그램 성능
2.1 내용 개요
  • 쓰기 프로그램의 주요 목표는 모든 가능한 상황에서 정확하게 작업을 할 수 있도록 하는 것이다.프로그래머는 반드시 뚜렷하고 간결한 코드를 써서 자신이 코드를 이해하고 유지보수할 수 있도록 해야 한다. 또한 다른 사람이 코드를 신속하게 검사하고 유지보수할 수 있도록 해야 한다.프로그램이 빨리 실행되는 것은 매우 중요한 고려 요소로 효율적인 프로그램을 작성하려면 다음과 같은 몇 가지를 해야 한다.
  • 1) 우리는 적당한 알고리즘과 데이터 구조를 선택해야 한다.
  • 2) 우리는 컴파일러가 효율적인 실행 코드로 변환할 수 있는 원본 코드를 효과적으로 최적화할 수 있도록 작성해야 한다.이 부분은 컴파일러의 최적화 능력과 한계성에 대한 이해가 중요하다.
  • 3) 처리 연산량이 매우 큰 계산에 대해 한 임무를 여러 부분으로 나누고 이 부분은 멀티코어와 멀티프로세서의 어떤 조합에서 병행적으로 연산할 수 있다.
  • 프로그램 최적화의 첫 번째 단계는 불필요한 작업을 없애고 코드가 원하는 임무를 최대한 효과적으로 수행하도록 하는 것이다. 이것은 불필요한 함수 호출, 조건 테스트와 메모리 인용을 없애는 것을 포함한다.이러한 최적화는 목표와 그 어떠한 구체적인 속성에 의존하지 않는다는 것을 주의해라.
  • 프로그램 최적화의 두 번째 단계는 프로세서의 작동을 이해하고 프로세서가 제공하는 지령급과 능력을 이용하여 여러 가지 지령을 동시에 집행해야 한다.프로그래머와 컴파일러는 목표 기계의 모델을 이해하고 명령을 어떻게 처리하는지, 그리고 각 조작의 시퀀스 특성을 설명해야 한다.예를 들어 컴파일러는 시차 정보를 알아야 곱셈 명령을 사용하는지, 아니면 위치 이동과 덧셈의 어떤 조합을 사용하는지 확인할 수 있다.현대 컴퓨터는 복잡한 기술로 기계급 프로그램을 처리하고 많은 명령을 병행하여 집행하는데, 집행 순서는 그들이 프로그램에 나타난 순서와 같지 않을 수도 있다.
  • 연구 프로그램의 어셈블리 코드는 컴파일러를 이해하고 코드가 어떻게 운행되는지 이해하는 가장 효과적인 수단 중 하나이다.내부 순환하는 코드를 자세히 연구하는 것은 좋은 시작으로 성능을 떨어뜨리는 속성을 식별한다. 예를 들어 과도한 메모리 인용과 레지스터에 대한 부적절한 사용이다.어셈블리 코드부터 우리는 어떤 조작이 병행적으로 실행될지, 그리고 그들이 프로세서 자원을 어떻게 사용할지 예측할 수 있다.그리고 나서, 우리는 소스 코드를 수정하고, 컴파일러를 제어하여 더욱 효율적인 실현을 시도해 보았다.

  • 2.2 컴파일러의 능력과 한계 최적화
  • 현대 컴파일러는 복잡하고 정교한 알고리즘을 이용하여 한 프로그램에서 계산된 값이 무엇인지, 그리고 그들이 어떻게 사용되는지 확인한다.그리고 표현식을 간소화하고 여러 곳에서 같은 계산을 사용하며 주어진 계산이 실행되어야 하는 횟수를 줄일 수 있는 기회를 이용한다.GCC컴파일러는 사용자에게 그들이 사용하는 최적화된 제어를 제공했다. 가장 간단한 제어는 최적화 단계를 제정하는 것이다.예:
  • -Og 호출 GCC는 GCC가 한 조의 기본적인 최적화를 사용하도록 하는 것이다.
  • -O1-O2-O3은 GCC를 호출하여 그로 하여금 더욱 많은 최적화를 사용하게 한다.
  • 이상의 방법은 프로그램의 성능을 더욱 향상시킬 수 있고 프로그램의 규모를 증가시킬 수도 있으며 표준적인 모뎀이 함께 가면 프로그램에 대한 모뎀을 더욱 어렵게 할 수도 있다.GCC를 사용하는 대부분의 소프트웨어 프로젝트에서 최적화 수준-O2는 이미 접수된 표준이 되었다.
  • 또한 컴파일러는 프로그램에 대해 안전한 최적화만 조심스럽게 사용해야 한다. 즉, 프로그램이 직면할 수 있는 모든 상황에 대해 C 언어 표준이 제공한 보증 아래 최적화된 프로그램은 최적화된 버전과 같은 행위를 한다.컴파일러가 안전한 최적화만 하도록 제한하는 것은 원하지 않는 실행 시 행동을 초래할 수 있는 일부 가능한 원인을 없앴지만, 이것은 프로그래머가 컴파일러가 유효한 프로그램과 코드로 변환할 수 있는 프로그램을 작성하는 데 더 많은 힘을 들여야 한다는 것을 의미한다.
  • 프로그램의 전환이 안전한지 결정하는 난이도를 이해하기 위해 다음과 같은 두 가지 과정을 볼 수 있다.
  • 	void twiddle1(long *xp, long *yp)
    	{
    		*xp += *yp;
    		*xp += *yp;
    	}
    
    	void twiddle2(long *xp, long *yp)
    	{
    		*xp += 2 * *yp;
    	}
    
  • 초보적으로 보면 이 두 과정은 같은 행위를 하는 것 같다. 모두yp가 가리키는 메모리의 값을 바늘xp가 가리키는 메모리에 두 번 추가한다.자세히 보면 함수 twiddle2의 효율이 더 높다.이것은 메모리 인용 (xp 읽기, yp 읽기, xp 쓰기) 을 3회만 요구하고, twiddle 1은 6회 (xp 읽기, yp 읽기, xp 쓰기 2회) 가 필요하다.따라서 컴파일러 컴파일러 과정인 twiddle1은 twiddle2를 기반으로 하는 계산이 더 효과적인 코드를 만들 수 있다고 생각할 것이다.
  • 그러나 xp=yp의 경우 twiddle1의 계산 결과는 xp가 가리키는 메모리의 4배, twiddle2의 계산 결과는 xp가 가리키는 메모리의 3배였다.컴파일러는twiddle1이 어떻게 호출될지 모르기 때문에 컴파일러 과정에서 파라미터 xp와yp가 같을 수 있다고 가정해야 한다.twiddle2 스타일의 코드를 twiddle1의 최적화 버전으로 만들 수 없다.
  • 이 두 바늘은 같은 메모리 위치를 가리킬 수 있는 상황을 메모리 별명으로 사용할 수 있다. 안전한 최적화만 실행할 때 컴파일러는 서로 다른 바늘이 메모리의 같은 위치를 가리킬 수 있다고 가정해야 한다.이것은 최적화를 방해하는 주요한 요소를 초래했다. 컴파일러가 최적화 코드 기회를 만드는 프로그램을 심각하게 제한할 수 있고 가능한 최적화 전략을 제한할 수 있다.예:
  • 	void swap(long *xp, long *yp)
    	{
    		*xp = *xp + *yp;	/* x+y */
    		*yp = *xp - *yp;	/* x+y-x = y */
    		*xp = *xp - *yp;	/* x+y-x - x */
    	}
    

    만약 이 과정을 호출할 때 xp=yp가 있다면 어떤 효과가 있습니까?
    두 번째 최적화를 방해하는 요소는 함수 호출이다.예는 다음과 같습니다.
    	long f();
    
     	long fun1()
    	{
    		return f()+f()+f()+f();
    	}
    
    	long fun2()
    	{
    		return 4*f();
    	}
    
  • 상기 두 과정의 계산이 모두 같은 결과인 것을 초보적으로 보았지만fun2는fun1회만 호출하고fun1은fun4회만 호출해야 한다.fun1을 원본 코드로 사용할 때fun2 스타일의 코드가 매우 생성되고 싶어 합니다.단, 아래 f의 코드를 고려하면:
  • 	long counter = 0;
    	
    	long f()
    	{
    		return counter++;
    	}
    
  • 이 함수는 전역 프로그램 상태의 일부분을 수정하는 부작용이 있을 것이다.그를 호출하는 횟수를 바꾸면 프로그램의 행동이 바뀐다.특히 시작할 때 전역 변수counter=0을 가정하면fun1에 대한 호출은 0+1+2+3=6을 되돌려주고fun2에 대한 호출은 4*0=0을 되돌려줍니다.
  • 대부분의 컴파일러는 하나의 함수에 부작용이 있는지 없는지를 판단하려고 하지 않는다. 최악의 경우 모든 함수의 호출이 변하지 않는 것을 가정할 것이다. 즉fun1의 원본 코드가fun2의 모양으로 최적화되지 않아fun2의 프로그램 성능을 향상시킬 것이다.
  • 비고: 내연 함수 교체 최적화 함수 호출을 사용하여 함수 호출을 포함하는 코드를 내연 함수 교체 과정으로 최적화할 수 있으며, 이때 함수 호출을 함수체로 대체할 수 있다.우리는 함수 f에 대한 네 번의 호출을 바꾸고fun1 코드를 펼친다.
  • 	long fun1in()
    	{
    		long t = counter++;	/* +0 */
    		long t = counter++;	/* +1 */
    		long t = counter++;	/* +2 */
    		long t = counter++;	/* +3 */
    	
    		return t;
    	}
    
  • 이상의 방법은 함수 호출 비용을 줄이고 전개된 코드에 대한 최적화를 허용한다.따라서 컴파일러는fun1in에서 전역 변수counter에 대한 업데이트를 통일시켜 이 함수의 최적화 버전을 만들 수 있다.
  • 	long fun1opt()
    	{
    		long t = 4 *counter + 6;
    		counter += 4;
    		
    		return t;
    	}
    
  • 컴파일할 때 명령줄 옵션인'-finline','-O1'또는 더 높은 등급의 컴파일 옵션을 사용할 때 최근 GCC버전은 상기 형식의 최적화를 시도한다.현재 GCC는 단일 파일에 정의된 함수의 내연만 시도합니다.이것은 일반적인 상황과 라이브러리 함수를 한 파일에서 정의하지만 다른 파일의 함수에 의해 호출될 수 없습니다.
  • 그러나 어떤 경우에는 컴파일러가 내연 교체를 실행하는 것을 막는 것이 가장 좋다.한 가지 상황은 기호 디버거로 코드를 평가하는 것이다. 예를 들어 GDB 디버그 프로그램, 함수가 내장 디버그로 최적화된 경우, 이 호출을 추적하거나 단점을 설정하는 시도는 실패할 것이다.또 다른 상황은 코드 분석 방식으로 프로그램의 성능을 평가하는 것이다.
  • 상기 설명에 대해 우리는 최적화 능력에 대해 알 수 있듯이 GCC컴파일러는 임무를 감당할 수 있다고 여겨지지만 최적화 성능이 특별히 뛰어나지 않다. 그는 기본적인 최적화를 완성할 수 있지만 프로그램에 대해 더욱'진취적인'컴파일러가 하는 그런 급진적인 변환을 하지 않을 것이다.따라서 우리는 코드를 작성할 때 컴파일러에 잘못된 뜻이 생기지 않는 코드가 있으면 기능에 영향을 주지 않는 토대에서 가능한 한 컴파일러가 효율적인 코드를 생성할 수 있도록 작성한다.
  • 좋은 웹페이지 즐겨찾기