(3) 문자열 가이드라인
스타일 가이드라인
스트링을 두번 초기화하지 말라
디폴트 스트링 타입인 AnsiString은 생성된 직후 자동으로 빈 값으로 초기화됩니다. 따라서 AnsiString을 다시 초기화할 필요는 없습니다. 예를 들어 아래 코드에서 s := ''; 부분은 불필요합니다.
procedure GreatestOnEarth;
var
S: string; // a long string, not short!
begin
S := '';
...
end;
주의할 것은, 이런 특성은 스트링을 리턴하는 함수들에는 적용되지 않는다는 것입니다. 이것은 함수의 result 변수가 지역 변수보다는 var 파라미터와 더 비슷하게 행동하기 때문입니다.
AnsiString에는 최대한 SetLength를 사용하라
동적 할당 특성은 AnsiString을 매우 강력하게 해줍니다. 불행히도, 이 강력함을 잘못 사용하기가 아주 쉽습니다. 그 전형적인 상황은 아래와 같습니다.
S2 := '';
for I := 2 to length(S1) do
S2 := S2 + S1[I];
이런 경우에 Delete를 쓸 수 있다는 점은 일단 논외로 하고, 여기서 문제는 S2 스트링의 메모리가 루프 내에서 반복적으로 계속 재할당된다는 것입니다. 당연히 이것은 시간을 소모합니다. 간단하고 잠정적으로 훨씬 효과적인 대안은 다음과 같습니다.
SetLength(S2, Length(S1) - 1);
for I := 2 to length(S1) do
S2[I-1] := S1[I];
여기서 S2의 메모리는 루프의 이전에서 단 한번만 할당됩니다.
이런 종류의 "메모리 매니저 오용"은 AnsiString에서 일반적으로 벌어지는 일인데, 그것은 재할당이 자동으로 이루어져 잘 무시되기 때문입니다. PChar에서는 직접 메모리를 할당하므로 프로그래머들은 이런 코딩 스타일과 관련된 문제가 쉽지 않다는 점을 잘 알고 있죠. 옛 파스칼 스타일 스트링은 정적 할당을 사용하므로 이런 문제가 없습니다.
스트링과 동적 배열의 쓰레드 안전성 (델파이 5 이후와 펜티엄 II/애슬론에 해당)
스트링과 동적 배열의 쓰레드 안전성(thread safety)은 레퍼런스 카운트 문제를 차단하여 개선되었습니다. 델파이 4 버전까지는 멀티 프로세서 환경에서 AnsiStrting의 레퍼런스 카운트가 쓰레드 안정성에 문제가 있었습니다. 이 문제는 레퍼런스 카운트를 직접 수정하고 선점을 방지하는 단일 명령을 락 함으로써 픽스되었습니다. 불행히도 모든 것에는 그 대가가 따릅니다. 이렇게 쓰레드 안전성을 위해 사용된 lock CPU 명령 프리픽스는 펜티엄 II 프로세서에서 상당히 많은 시간을 소모합니다. 이 수정으로 인한 효과를 필자가 측정한 바에 따르면, 한 레퍼런스 카운트 조정 때마다 28 사이클이 추가되었습니다. 이로 인해 최악의 경우 2배의 성능 저하가 나타날 수 있습니다. 실제 사례에서는 20% 범위의 영향이 나타났습니다.
델파이 4까지의 AnsiString처럼 되돌리기
위에서 설명한 긴 스트링(AnsiStrting)의 동작을 델파이 4 이전처럼 되돌리면 긴 스트링의 속도가 더 빨라집니다. 이렇게 하려면, system.pas의 내용을 약간 수정하고 재컴파일해야 합니다. system.pas를 재컴파일하는 가장 쉬운 방법은 make 유틸리티와 /source/Rtl 디렉토리에 있는 makefile을 이용하는 것입니다. 원래의 파일들을 남겨두기 위해 소스 파일을 새 디렉토리로 복사합니다. make는 또한 lib, bin 등 다른 서브디렉토리들을 필요로 합니다. 새 위치에 이런 서브디렉토리들을 생성하십시오. 또한 함께 컴파일되어야 하는 많은 외부 어셈블리 파일들이 있으므로 TASM도 필요합니다.
변경할 내용은 상당히 간단합니다. 먼저, 모든 lock 프리픽스를 제거해야 합니다. 필자는 'lock'을 '{lock}'로 전체 치환하는 방법을 좋아합니다. 이렇게 하면 스트링과 동적 배열을 델파이 5보다 이전의 성능 수준으로 되돌립니다. 성능을 더욱 높이려면 xchg 명령 두 개를 제거해야 합니다. 이 명령은 암시적으로 lock 프리픽스를 발생시킵니다. 원래의 코드는 아래와 같습니다.
procedure _LStrAsg(var dest: AnsiString; source: AnsiString);
...
@@2: XCHG EDX,[EAX]
...
procedure _LStrLAsg(var dest: AnsiString; source: AnsiString);
...
XCHG EDX,[EAX] { fetch str }
...
두 경우 모두 세 개의 mov 명령과 ecx를 임시 레지스터로 사용함으로써 XCHG 명령을 대체할 수 있습니다.
procedure _LStrAsg(var dest: AnsiString; source: AnsiString);
...
@@2: { XCHG EDX,[EAX]}
mov ecx,[eax]
mov [eax],edx
mov edx,ecx
...
procedure _LStrLAsg(var dest: AnsiString; source: AnsiString);
...
{XCHG EDX,[EAX]} { fetch str }
mov ecx,[eax]
mov [eax],edx
mov edx,ecx
...
위와 같이 변경하면, 스트링 대입이 델파이 5에서보다 6배 빠르게 실행됩니다. (델파이 4보다는 2배 빠릅니다)
ShortString 사용을 피하라 (델파이 5 이후)
아마도 오래된 ShortString 방식을 점차 몰아내고 스트링 조작 루틴을 한 세트만 유지하기 위해서라고 보이는데, ShortString은 많은 조작이 들어갈 때는 긴문자열로 변환됩니다. 이런 이유로 이런 ShortString 작업은 훨씬 느려지게 됩니다.
동적 임시 문자열이 생기는 Copy 사용을 피하라
이것도 메모리 매니저의 오용과 관련된 문제입니다. 일반적인 상황은 아래와 같습니다.
if Copy(S1,23,64) = Copy(S2,15,64) then
...
여기서 문제는 임시 문자열을 위해 메모리 할당이 일어나고, 그래서 시간을 잡아먹는다는 것입니다. 이것은 유감스러운 일이지만, 네이티브 AnsiString 함수들에서는 아래와 같은 방식 외에는 다른 대안을 별로 제공해주지 않고 있습니다.
I := 1;
Flag := False;
repeat
Flag := S1[I+22] <> S2[I+14];
Inc(I);
until Flag or (I>64);
if not Flag then
...
AnsiString만을 사용하고 필요하면 PChar로 캐스트하라
AnsiString은 어찌됐든 본질적으로 그다지 효율적이지 못하다는 근거 없는 믿음이 많이 퍼져 있는데요. 이런 믿음은 잘못된 코딩 관행이나 메모리 매니저 오용, 그리고 위에서 설명한 네이티브 지원 함수의 부족 때문에 나온 것입니다. AnsiString은 일단 동적으로 할당이 일어나고 나면 다른 문자열과 별 다를 바가 없습니다. 메모리에 선형의 바이트들로 이루어지므로, 딱히 더 효율적이거나 덜 효율적이지도 않습니다. 충분한 지원 함수들과 적절한 코딩을 한다면 다른 스트링에 비해 AnsiString의 성능의 차이는 무시할 정도입니다.
(역주: 델파이 2009 이후 버전에서는 AnsiString은 PChar가 아닌 PAnsiChar로 캐스트해야 합니다)
스트링의 끝을 제거하기 위해서는 Copy보다 Delete를 선호하라
Copy 함수는 항상 전체 스트링을 복사합니다. 하지만 Delete는 현재 스트링의 끝 부분을 잘라낼 뿐입니다.
AString := Copy(AString, 1, Length(AString)-10);
Delete(AString, Length(AString)-10, 10);
</pre>
위 두 라인의 코드는 동일한 역할을 하지만 Delete를 사용한 경우가 더 빠릅니다.
스트링 합치기
스트링들을 합치는 가장 좋은 방법은 아주 간단합니다. s1 := s2+s3+s4; 이런 방식이 가장 좋은 결과를 냅니다. 합치는 스트링의 갯수가 몇개이든, 또 그 안에 컴파일타임 상수가 있든 없든 마찬가지입니다.
노트: 델파이 2에서는, 컴파일타임 상수가 관련되어 있을 경우 s1 := Format([%s%s],s2,s3) 이런 방식이 더 빠를 수도 있습니다.
PChar로의 캐스팅
본질적으로, 스트링을 PChar로 변환하는 데에는 3가지 방법이 있습니다. PChar로 타입 캐스트하는 방법, 첫번째 문자의 주소를 이용하는 방법, 스트링을 일반 포인터로 타입 캐스트 하는 방법 등입니다. 이 세가지 방법의 실제 동작은 모두 다 다릅니다.
첫번째 문자의 주소를 가져오는 방법(예: p:=@s[1];)은 리턴되는 PChar가 오직 해당 스트링 변수만이 가리키는 유니크한 문자열을 가리키도록 하기 위해 UniqueString을 호출하게 됩니다. 스트링을 PChar로 타입 캐스트하는 방법은 첫번째 문자의 주소를 리턴하거나 혹은 스트링이 비어있는 경우 null의 주소를 리턴합니다. 따라서 PChar 값은 항상 nil이 아니게 됩니다. 가장 간단한 방법은 일반 포인터(예: p:=pointer(s);)로 캐스팅하는 방법입니다. 이 방법에는 숨겨진 함수 호출이 일어나지 않기 때문에 가장 빠릅니다.
(역주: 델파이 2009 이후 버전에서는 AnsiString은 PChar가 아닌 PAnsiChar로 캐스트해야 합니다)