델파이 최적화 가이드라인 (4) – 부동소수점 가이드라인

아래 글은 원래 2002년에 Robert Lee의 홈페이지인 http://www.optimalcode.com 라는 사이트에 실렸던 “Delphi Optimization Guideline”이라는 글의 일부입니다. 지금 이 사이트는 없어진 상태이고 원래의 필자도 전혀 연락이 안되는 상태입니다. 하지만 최근에 예전의 컨텐츠들을 http://delphitools.info 에서 되살렸습니다. 델파이 개발자분들께 도움이 될 부분이 많을 것 같아서 번역을 시작해봅니다. 전체는 4개의 시리즈로 되어 있으며, 이 글은 그중 마지막인 Floating Point Optimization Guideline 입니다. 

원문 : http://delphitools.info/OptimalCode/float.htm

사용자 삽입 이미지
Delphi Optimization Guidelines

(1) 일반 가이드라인
(2) 정수 가이드라인
(3) 문자열 가이드라인
(4) 부동소수점 가이드라인

(4) 부동소수점 가이드라인

스타일 가이드라인

절대적으로 필요한 경우가 아니면 extended를 사용하지 말라

extended의 정밀도(80비트)는 내부적으로 FPU가 계산을 수행하기는 하지만 로드와 저장에 있어 그다지 효율적이지 못합니다. 따라서 extended 타입을 사용하는 것은 단순한 산술 연산(+, -, *)의 전체 실행 시간을 두배까지 늘릴 수 있습니다. 이것은 연산 자체를 수행하는 데 들어가는 추가 시간 때문이 아니라 extended 값을 로드하고 저장하는 데 들어가는 추가 시간입니다. 또한, extended 타입 변수의 크기의 불편함(10바이트,
doubleword 정렬에서 12바이트를 차지) 때문에 캐시 라인에도 비효율적이 될 가능성이 높아지며 그로 인해 성능 손실이 생기게 됩니다.

마지막으로, 델파이 버전 2에서 4까지에서는, extended 타입의 지역 변수는 할당된 12바이트(3 dword) 중 첫 10바이트가 아니라 뒤의 10바이트로 정렬되는 문제가 있습니다. 이것은 extended가 지역 변수로서는 잘못 정렬된다는 뜻입니다. 이 문제는 델파이 5 버전에서 픽스되었지만, 아직도 컴파일러가 생성한 임시 변수들에서는 문제가 남아있습니다.

부동소수점 타입들을 섞지 말라

기본적인 문제는, 두 가지 경우에 불필요한 타입 변환 단계를 강제하게 된다는 것입니다. 1) 한 변수를 다른 변수에 대입, 2) 변수를 파라미터로 전달. 이런 경우, 한 변수의 두 인스턴스가 FP 스택에 로드되어야 하며 그냥 복사되는 대신 새로운 타입으로 저장되게 됩니다. 이로 인해 3, 4배 시간을 더 소모하게 됩니다.

각 대입문에서 함수 호출은 한번으로 제한하도록 노력하라

FPU(부동소수점 유닛)의 레지스터 스택은 8개 밖에 없습니다. 따라서 스택이 넘치는 것을 막기 위해, 하나의 표현식 내의 함수 호출이 일어나면 호출을 실행하기 전에 레지스터 스택을 먼저 비워야 합니다. 오직 하나의 예외는 인자들이 결정된 직후 그리고 나머지 표현식의 나머지 부분이 평가되기 전에 호출될 수 있어서 표현식 내의 첫번째 함수 호출이 이런 비우기 작업과 무관할 경우입니다. 델파이는 모든 저장된 값들을 임시적인(그리고 보이지 않는) extended 변수에 저장함으로써 스택을 비웁니다. 앞에서 설명했던 것처럼, extended는 성능이 낮으므로, 개발자가 스스로 임시 변수를 만들고 한 변수 대입마다 하나의 함수 호출만 일어나도록 표현식을 나누어야 합니다. 이 규칙은 System 유닛에 있는 컴파일러 “매직” 함수에서도 볼 수 있는데, Abs나 Sqr 함수 등입니다. 이것은 “중첩된” 호출을 포함하지 않는데, 파라미터 표현식에 포함된
함수 호출이 다른 함수 호출을 가지는 경우를 말합니다. 부동소수점 파라미터는 항상 스택을 통해 전달되므로, 각 파라미터 표현식은 분리된 표현식을 나타냅니다.

부동소수점 상수

부동소수점 상수는 실행 파일 내에 특정 타입으로 저장됩니다 (single, double or extended). 기본적으로, 정수 부분만으로 된 상수는 single로 저장되고 소수점 아래 부분이 있는 상수는 extended로 저장됩니다. 앞에서 언급한대로, extended를 사용하는 것은 높은 비용이 들기 때문에, 타입 지정 상수(typed constant)로 만들어서 지정된 크기(single or double)가 되도록 해야 합니다. 해당 값은 어떤 식으로든 바이너리에 포함되므로 전체 실행파일의 크기는 늘어나지 않습니다. 예를 들어봅시다.

위 코드는 extended 타입을 사용하는 코드보다 빠를 뿐만 아니라 더 작습니다(double은 8바이트만 사용). $J+ 지시어를 사용하면 타입 지정 상수의 값도 변경할 수 있다는 점도 기억해둡시다.

또한, 컴파일러는 컴파일 중에 가능하다면 상수들을 결합합니다. 두 상수 사이의 연산이 그 상수들과 변수들, 변수 표현식 등이 관련된 다른 다른 연산들보다 우선순위에서 높을 경우, 그 상수들은 상수 폴딩(constant folding)이 됩니다. 추가로, 델파이 2와 3에서는 상수로 나누는 나눗셈은 항상 곱셈으로 바뀝니다. 불행히도 이 기능은 4 버전에서 없어졌습니다. 따라서, 예를 들면 델파이 2와 3에서 다음의 문장은,

실제로는 아래와 같이 계산됩니다.

델파이 4에서는 위의 문장은 아래와 같이 계산됩니다.

변수들보다 상수를 앞에 두면 상수 폴딩의 효과가 더 나아집니다.

위 코드는 실제로는 아래와 같이 계산됩니다.

 

컨트롤 워드 정밀도를 적절한 레벨로 설정하라

부동소수점 나눗셈과 제곱근 명령은 상당한 시간을 소모합니다. 하지만, 최대의 정확도가 필요한 경우가 아니라면 일정 시간을 아낄 수 있습니다. FPU의 컨트롤 워드를 변경하면 정확도의 수준을 바꿀 수 있습니다. 델파이 런타임 라이브러리에 의해 초기화되는 기본 정확도는 가장 느리지만 가장 정밀합니다(즉, extended). 델파이는 Set8087CW 프로시저와 전역 변수 Default8087CW를 통해 FPU의 컨트롤 워드에 대한 직접 변경을 지원합니다. 컨트롤 워드를 다른 정밀도 레벨로 바꾸려면 다음과 같이 하면 됩니다.

컨트롤 워드를 변경하면 나눗셈의 실행 시간만 달라지며, 펜티엄 II와 펜티엄 III 프로세서에서는 제곱근의 실행시간도 영향을 받습니다. 델파이 6 이후로는 간단히 SetPrecisionMode()를 호출하면서 적절한 정밀도 레벨 상수(pmSingle, pmDouble, pmExtended)를 넘겨주기만 하면 됩니다.

 

Trunc보다 Round를 선호하라

Trunc는 FPU 컨트롤 워드를 읽고 쓰므로 대단히 느립니다. 반면 Round 함수는 이런 동작을 하지 않으므로 Pentium II의 경우 2.5배 정도 빠릅니다.

 

함수보다는 var 파라미터를 가진 프로시저를 선호하라

이것은 오버헤드 관리 이슈이며, 따라서 전체 처리 시간에서 오버헤드가 더 많은 비율을 차지하는 작은 함수들의 경우에 적용됩니다. 다음의 예에서,

아래와 같이 바꾸면,

처리 시간을 절반으로 줄일 수 있습니다(펜티엄 II 기준). 이것은 넘겨주는 값을 실질적으로 사용하기보다는 주로 값을 넘기기만 하는 경우(단순한 대입 등) 더 효과적입니다. 예를 들면,

위 코드의 실행 시간은 오버헤드가 대부분을 차지하게 됩니다.

이 테크닉의 불리한 점은, 변경되지 말아야 할 파라미터들에 대해서도 const 대신 var를 사용해야 한다는 것입니다. const는 컴파일 타임에 파라미터가 확실히 변경되지 않는지 여부만 확인할 뿐 부동소수점 파라미터에 대해 실질적으로 아무것도 하지 않습니다.

부동소수점 예외

FP 예외(divide by zero 등)는 에러가 발생한 순간에 일어나지 않습니다. 대신 다음 부동소수점 명령까지 지연됩니다. 아마도 이렇게 구현한 이유는 해당 에러를 테스트하고 처리할 수 있도록 하기 위해 사용되었던 것으로 생각됩니다. 하지만, 이로 인해 예외가 실제로 일어났을 때 엉뚱한 코드가 문제가 있는 것으로 보이게 되는 어이없는 상황이 벌어집니다. 이 문제에 대한 해결책은 예외를 강제로 발생시키기 위해 대기나 FWait 명령을 끼워넣는 것입니다. 이것은 컴파일러가 부동소수점 문 이후 수행하는 것과 동일한 것입니다. 이렇게 대기를 실행하는 것은 당연히 시간을 소모하기 때문에, 일단 디버그를 마치고 나면 직접 작성한 부동소수점 어셈블리 루틴에서 루틴의 끝에 하나만 추가하기를 원할 수도 있습니다. 이렇게 하면 비용을 낮추면서도 발생한 모든 예외가 정확한 루틴을 가리킬 수 있게 됩니다.

물론, 모든 규칙에는 예외가 있습니다. 이 규칙이 적용되지 않는 예는, 윈도우 95에서 아래의 코드를 실행했을 때입니다(Stefan Hoffmeister의 FPU 데모에서 발췌)

윈도우 95와 달리 NT에서는 이 코드는 정확하게 FP 예외를 발생시킵니다. 윈도우 95에서는 FWAIT 이전에 FXAM를 끼워넣으면 예외가 발생합니다. 고마워, 마이크로소프트.

 

최적화 테크닉

 

스스로 부동소수점 최적화를 해야 한다

델파이는 부동소수점 최적화를 수행하지 않습니다. 여러분이 작성한 코드 그대로 실행되게 됩니다. 따라서 일반적인 표현식들이 합쳐질 것이라는 등의 추측을 하지 마십시오. 여러분이 직접 수행해야 합니다.

나눗셈을 줄이기 위해 노력을 다하라

나눗셈은 곱셈, 덧셈, 뺄셈에 비해 20~40배의 시간이 들 정도로 매우 큰 비용이 듭니다. 가능할 때는 항상 나눗셈을 루프 바깥으로 옮겨야 합니다. 상수로 나눗셈을 할 때는 그와 동등한 곱셈으로 바꾸는 것을 잊지 마십시오.

부동소수점수의 0 비교를 피하는 방법

부동소수점 변수내에 0이 들어있는지 확인할 때, 특정한 어떤 상황에서는 직접 비교를 피하고 대신 타입 캐스트를 이용하는 것이 이득일 수 있습니다. 이것은 부동소수점 비교가 0이 저장된 방식을 이용하는 부동소수점 기반의 0 확인을 필요로 하기 때문입니다. 이 테크닉은  가독성을 상당히 저하시키므로 절제해서 사용해야 합니다.

single 변수가 0인지 확인하려면 다음과 같은 코드를 사용합니다.

double 변수를 확인하는 방법은 좀 더 복잡합니다.

이 테크닉은 펜티엄 II에서 비교 시간을 30~40% 정도 줄여줍니다.

1 comment for “델파이 최적화 가이드라인 (4) – 부동소수점 가이드라인

  1. (4) 부동소수점 가이드라인

    스타일 가이드라인

    절대적으로 필요한 경우가 아니면 extended를 사용하지 말라

    extended의 정밀도(80비트)는 내부적으로 FPU가 계산을 수행하기는 하지만 로드와 저장에 있어 그다지 효율적이지 못합니다. 따

답글 남기기

이메일 주소는 공개되지 않습니다.