델파이 고성능 구현 (High Performance Delphi) 를 요약했습니다. (이 요약 번역은 원본 비디오와 내용이 일부 다르거나, Q&A등 일부 생략되었을 수 있습니다.)

 

이 요약에는 설명된 코드 중 중요한 부분만 적어두었습니다. 실제로 적용하기 전에는 제공되는 전체 코드를 받아서, 언급되지 않는 부분도 확인하기 바랍니다. (전체 코드라고 해도 매우 짧습니다)

 

델파이 고성능 구현 ‘Delphi High Performance’ 도서 저자의 성능 개선을 데모와 함께 알려줍니다. 여러분 코드에도 적용할 것이 있을 것입니다.

  • 성능 향상 개요
  • 성능 향상 중 알고리즘 향상 예시
    • 더 좋은 알고리즘 선택하기
    • 불필요한 코드 실행 줄이기
    • 불필요한 코드 실행 없애기

발표자 (Primož Gabrijelčič)는 1980년대 8비트 시절부터 파스칼 코드를 써오고 있습니다. 주로 방송용 고가용성 서버 프로그램을 개발하고 있으며, 수준높은 주제를 다루는 수많은 기고를 해오고 있습니다. (웹페이지: http://thedelphigeek.com)

 

발표자가 저술한 도서:

  • 디자인 패턴을 델파이에서 실습하기(Design patterns with Delphi): https://www.packtpub.com/product/hands-on-design-patterns-with-delphi/9781789343243
  • 델파이 고성능 구현 (Delphi High Performance): https://www.packtpub.com/product/delphi-high-performance/9781788625456
  • 옴니쓰레드라이브러리를 활용한 병렬 프로그래밍 (Parallel programming with OmniThreadLibrary): https://www.thedelphigeek.com/2018/02/parallel-programming-with.html

 

원본 비디오 시청: https://delphicon.embarcadero.com/talks/high-performance-delphi/

 

성능 향상 개요

’성능 향상’이 의미하는 바는 사람들마다 다를 수 있다. 하지만, ‘성능 향상 작업’을 하는 순서는 다음과 같다.

  • 문제 식별 (‘측정’이 중요하다!)
    • TStopwatch, 성능 카운터, GetTickCount 등을 활용하여 명시적으로 측정하자.
    • AQTime 등 전문 프로파일링 도구를 사용하는 것도 좋다.
  • ‘알고리즘 향상’: 가장 좋은 방법!!! (알고리즘 향상이 쉽지 않은 경우, 아래의 방법도 검토하자)
  • 세부적인 코드 튜닝
  • 병렬 처리 적용
  • 외부 라이브러리 사용
    • 더 좋은 알고리즘을 제공하는 라이브러리를 도입하여 빠르게 문제 해결 가능.
    • 주의할 점: 외부 라이브러리 제작자가 유지보수를 하지 않는 상황에 대한 대비가 필요
  • 어셈블러와 같은 저수준 언어로 (성능 향상이 필요한 부분을) 대체
    • 주의할 점: 10년 후에는 지금보다도 저수준 언어 개발자 찾기가 더 어려워 진다는 점을 고려해야 함

알고리즘 향상

이 세션에서는 유용한 2가지를 예제와 함께 살펴본다.

  • ‘더 좋은 알고리즘 적용’
  • ‘불필요한 코드 실행 줄이기와 없애기

 

알고리즘 복잡도

빅오(Big-O) 표기법 (역자주: 빅오에 대한 설명은 굳이 번역하지 않았습니다)

  • 데이터가 증가할 수록 프로그램이 어떻게 느려질 것인지를 판단할 때에도 유용하다.
  • 프로그램의 성능 문제는 데이터가 많아지면서 발생하는 경우가 많다.

*관련 웹페이지 추천:

  • http://bigocheatsheet.com: 알고리즘 별 복잡도가 잘 정리되어 있다.
  • https://geeksforgeeks.org/data-structures: 다양한 데이터 구조에 대한 좋고 긴 토론이 정리되어 있다.

DelphiCon-HighPerformance1Big-O.png

<알고리즘 별 데이터 증가와 복잡도(성능 저하)의 관계> 

O(1)은 데이터가 아무리 증가해도 성능이 저하되지 않는다. 피보나치 수열 재귀호출은 급적하게 저하된다 (역자 주: 재귀 호출은 매우 강력하지만, 코딩 시 매우 주의해야 합니다. 이 세션에서도 재귀 호출과 관련된 예시와 개선 방향이 제시됩니다)

 

더 좋은 알고리즘 예제: 무작위 단어 검색 프로그램의 성능을 향상해보자

글자수가 주어지면, 프로그램은 그 글자수에 해당하는 무작위 알파벳 조합 단어가 37만개 단어가 목록 안에 있는 지를 찾는 프로그램.(10초가 지나도 못찾으면 시간 초과를 표시)

 

알고리즘

1 이내 결과 

1 이상 결과

시간 초과

비고

(무작위 단어를 만들고) 정렬되지 않은 TStringList 검색

~ 4글자

5글자

6글자

표에서 O(n) 해당; TStringList.Sorted := false

 

최악의 경우, 37만번째 단어까지 찾아봐야 결과를 알 수 있다.

(무작위 단어를 만들고) 정렬된 TStringList 검색

~ 6글자

7글자

8글자

위 표에서 O(log n)에 해당; TStringList.Sorted := true; 

 

false 보다는 목록 생성 시간이 오래 걸리지만 검색 속도는 빠르다. 

(무작위 단어를 만들고) TDictionary 사용

~ 7글자

8글자

9글자

 

(무작위 단어 조차 만들지 않고) 글자 수별로 TStringList를 따로 만들어서 사용

거의 무제한

(해당 없음)

(해당 없음)

성능 개선을 위한 알고리즘을 창의적으로 만들자.

 

* 역자주: 원문 링크에서 이 비디오에서 보여준 코드 전체를 다운로드 받을 수 있습니다. 여기서는 중요한 코드만 뽑아서 요약했습니다.

<코드 비교(검색 부분)>

 

// 정렬되지 않은 TStringList 검색

FindGoodWord (

  function (const word: string): boolean

  begin

    Result := FWordsUnsorted.IndexOf(word) >= 0;

  end);

 

// 정렬된 TStringList 검색

FindGoodWord (

  function (const word: string): boolean

  begin

    Result := FWordsSorted.IndexOf(word) >= 0;

  end);

 

//TDictionary 검색

FindGoodWord (

  function (const word: string): boolean

  begin

    Result := FWordsDictionary.ContainsKey(word) >= 0;

  end);

 

//글자 수별로 TStringList를 따로 만들고, 이 TStringList를 다시 오브젝트 리스트에 모아두고 사용하기

wordLen := inpWordLength.Value;

if (wordLen < 1) or (wordLen >= FWordsLOL.Count) then

  idx := -1

else //글자수가 0보다 크고, 그 글자수에 해당하는 목록 자체가 있으며, 꺼낼 때 쓸 임의의 숫자 설정

  idx := Random(FWordsLOL[wordLen].Count);

 

if (idx >= 0) and (idx < FWordsLOL[wordLen].Count) then

  lbWords.ItemIndex := lbWords.Items.Add(Format('%s (%d ms)', [FWordsLOL[wordLen][idx], time.ElapsedMilliseconds]))

else lbWords.ItemIndex := lbWords.Items.Add(Format('No such word (%d ms)', [time.ElapsedMilliseconds]));

 

<코드 비교(목록 생성 부분)>

목록을 만드는 코드보다 검색하는 코드가 훨씬 더 많이 실행된다는 점을 고려하자.

 

//참고: wordList는 파일에서 읽어서 만든 TStringList이다.

// 정렬되지 않은 TStringList 생성

FWordsUnsorted := TStringList.Create;

FWordsUnsorted.Assigned(wordList);

 

// 정렬된 TStringList 생성

FWordsSorted := TStringList.Create;

FWordsSorted.Assigned(wordList);

FWordsSorted.Sorted := True;

 

//TDictionary 생성

FWordsDictionary := TDictionary.Create(wordList.Count);

for word in wordList do

  FWordsDictionary.Add(word, True); //Key에 단어를 넣고, Value는 사용하지 않으므로 True로 모두 넣는다.

 

//글자 수별로 TStringList를 따로 만들고, 이 TStringList를 다시 오브젝트 리스트에 모아두고 사용하기

FWordsLOL := TObjectList.Create;

FWordsLOL.Add(nil);

for word in wordList do begin

  while FWordsLOL.Count <= Length(word) do

    FWordsLOL.Add(TStringList.Create);

    FWordsLOL[Length(word)].Add(word);

  end;

 

불필요한 코드 실행 줄이기

  • (일반적인 GUI 프로그램에서는) 초당 수천번씩 화면을 업데이트하지 말자.
  • BeginUpdate와 EndUpdate를 호출하자
  • (멀티 쓰레드 사용 시) 초당 수백만번 씩 운영체제에 메시지를 보내지 말자

불필요한 코드 실행 줄이기 예제: 2GB 파일을 1KB 단위로 읽고, 진행율을 표시하는 프로그램

<개선 전(흔한 코드)>

ProgressBar1.Max := CFileSize;

ProgressBar1.Position := 0;

total := 0;

while total < CFileSize do begin

  block := CFileSize - total;

  if block > 1024 then

    block := 1024;

  // reading 'block' bytes

  Inc(total, block);

 

    ProgressBar1.Position := total;

    ProgressBar1.Update; //문제 지점: 2백만번이나 화면을 업데이트하고 있었다 (이 부분만 주석처리하고 실행하면 바로 알 수 있다)

end;

 

<개선 후(화면 업데이트를 줄여서 속도를 보다 향상할 수 있다)>

ProgressBar1.Max := 100; //화면 업데이트를 100번만 해도 사용자에게는 충분하다.

ProgressBar1.Position := 0;

lastPct := 0; total := 0;

while total < CFileSize do begin

  block := CFileSize - total;

  if block > 1024 then

    block := 1024;

  // reading 'block' bytes

  Inc(total, block);

  currPct := Round(total / CFileSize * 100); //백분율을 미리 계산하고

  if currPct > lastPct then //1% 단위로만 (총 100번) 화면 업데이트

  begin

    lastPct := currPct;

    ProgressBar1.Position := currPct;

    ProgressBar1.Update; end;

  end;

 

불필요한 코드 실행 줄이기 예제: TListBox와 TMemo에 각각 10,000 개 항목을 넣는 프로그램

(참고:화면에 항목 하나를 표시하는 것은 그만큼 프로그램이 윈도우 운영체제와 메시지를 주고 받는다는 의미이다.

 

<개선 전 후 소요 시간 비교>

코드

TListBox 실행 시간

TMemo 실행 시간

루프 안에서 콘트롤에 10,000 항목 추가

7

28

루프 앞뒤에 BeginUpdate EndUpdate 호출

0.5

3

루프 안에서는 화면이 없는 중간 매체(TStringList 사용)에 항목을 넣고, 루프 밖에서 TMemo 한번에 넣기

(해당 없음)

0.2

가상리스트 박스 (이것은 불필요한 코드 실행 없애기에서 따로 설명)

0.003

 

 

BeginUpdate와 EndUpdate로 항목 추가 코드 루프를 감싸면 생기는 일

델파이는 (뭔가 변경을 시작할께요 라는 표시인) BeginUpdate 코드를 만나면 윈도우 운영체제에 메시지를 보내는 작업을 중단하고, (프로그램이 변경이 다 끝났어요 라는 표시인) EndUpdate를 만나야 윈도우에 메시지를 보내기 때문이다.

 

// 루프 앞뒤에 BeginUpdate와 EndUpdate 호출 코드 (3초로 개선됨)

Memo1.Lines.BeginUpdate;

for i := 1 to CNumLines do

  Memo1.Lines.Add('Line ' + IntToStr(i));

Memo1.Lines.EndUpdate;

 

// 루프 안에서는 TStringList에 항목을 넣고, 루프 밖에서 TMemo에 한번에 넣는 코드 (0.2초로 개선됨)

sl := TStringList.Create;

for i := 1 to CNumLines do

  sl.Add('Line ' + IntToStr(i));

Memo1.Text := sl.Text; FreeAndNil(sl);


불필요한 코드 실행 없애기

  • UI 가상화: 가상 Listbox, 가상 TreeView 등, 기능은 모두 하면서 불필요한 실행 제거
  • BeginUpdate와 EndUpdate를 호출하자
  • 캐싱 활용

불필요한 코드 실행 없애기 예제: (UI 가상화) 가상 리스트 박스

앞에서 TListbox(와 TMemo)에 10,000개 항목을 넣는 루프의 앞뒤를 BeginUpdate와 EndUpdate로 감싸서 속도가 현격하게 빨라지는 것을 살펴보았다. 하지만, VirtualListbox를 사용하면 더 향상할 수 있다.

 

여전히 리스트박스에 10,000 항목을 넣고 있는데, 실제로 화면에서 한번에 표시되는 항목은 18개이다. 다른 항목을 보려면 스크롤해야 보인다.

그렇다면 스크롤에 맞는 항목만 즉시 제공할 수 있다면, 10,000개 항목을 미리 가지고 있는 것과 다를 바가 없다.

 

그렇게 한 결과, 같은 기능을 가진 리스트박스가 0.007초 만에 실행됨 (앞서 BeginUpdate사용 시 0.5초 걸렸음)

 

<가상 리스트박스 구현 방법>

  • TListbox 설정 변경
    •  프로퍼티: Style 프로퍼티를 lbVirtual로 변경 (기본값은 lbStandard)
    • 이벤트: OnData, OnDataFind, OnDataObject 구현
      • OnData: 리스트박스에 값이 제공되는 이벤트
      • OnDataFind: Listbox.Items.IndexOf 함수가 사용될 때 호출되는 이벤트
      • OnDataObject: (리스트박스에 오브젝트가 들어가 있는 경우 사용한다. 지금은 생략)

// 10,000개 항목은 TStringList에만 넣어두고, 리스트박스의 갯수에는 전체 항목수 (10,000)으로 지정

FList.Capacity := CNumLines;

for i := 1 to CNumLines do

  FList.Add('Line ' + IntToStr(i));

ListBox1.Count := CNumLines;

 

//리스트박스는 항목을 표시하려고 할 때, 리스트박스는 OnData 이벤트가 발생하고 표시할 항목의 인덱스 번호를 전달한다.

//이때 TStringList의 해당 인덱스에 해당하는 값을 데이터로 반환하도록 코드를 쓴다.

procedure TfrmVirtualListBox.ListBox1Data(Control: TWinControl; Index: Integer; var Data: string);

begin

  Data := FList[Index];

  //로그를 통해 실제 호출되는 인덱스 확인하기,

  // 처음에는 18개 항목만, 스크롤 하면 해당 항목만 가져오는 것을 알 수 있다.

  //OutputDebugString(PChar(Index.ToString)); //일단 주석처리 함

end;

 

//마찬가지로 (TListbox).Items.IndexOf([찾고자 하는 값]) 코드가 사용되는 경우를 대비하여

//데이터를 실제 가지고 있는 TStringList의 IndexOf 결과를 반환하도록 코드를 쓴다.

//리스트박스는 실제로 항목을 가지고 있지 않다는 점을 명심하자

function TfrmVirtualListBox.ListBox1DataFind(Control: TWinControl; FindString: string): Integer;

begin

  Result := FList.IndexOf(FindString);

end;

 

위와 같이 성능이 문제라면 문제되는 부분 (이 예제에서는 10,000개 항목을 리스트박스에 넣는 부분)을 아예 없애는 것을 고려하자.

 

불필요한 코드 실행 없애기 예제: (캐싱) 지역변수를 활용한 캐싱

앞서 사용한 FindGoodWord 함수에서 사용자가 지정한 단어의 글자수는 코드에서 여러번 사용된다.

그 때마다 TEdit에서 읽어오는 대신 한번만 읽고 WordLen이라는 지역변수에 넣어서 사용한다.

특히, 값을 계산하거나 가져오는 시간이 오래 걸리는 것일 수록 지역변수를 활용한 캐시 효과는 크다.

그리고 실제로 의외로 많은 코드에서 이런 부분이 간과되어 있다.

 

불필요한 코드 실행 없애기 예제: (캐싱) 피노나치 수열 계산 시 한번 계산된 값은 배열에 넣어 두기

피보나치 수열 계산 원리는 재귀 호출이다. 하지만, 프로그래밍에서 그대로 재귀 호출로 구현하는 것은 비효율적이다.

재귀 호출로 인한 성능 문제를 해소하기 위해 앞에서 설명한 것처럼 알고리즘을 개선하는 것이 좋다.

 

대체로 재귀 호출 대신 루프를 사용하여 이 문제를 해소할 수 있다. (코드는 비디오의 54분01초의 위치 참조)

 

하지만, 혹시 더 좋은 알고리즘을 찾을 수 없다면, 계산을 반복하지 않도록 캐싱을 검토해볼 수 있다.

피보나치 수열 계산을 그대로 재귀 호출을 하더라도, 한번 계산된 값을 배열에 넣어두어서 바로 꺼내쓰도록 하면 재귀 호출 시 같은 계산을 반복하지 않는다. 즉 O(1)이 된다. (코드는 비디오의 52분 17초 위치 참조)

O(1)을 구현한 캐싱 딕셔너리 (사용하기도 쉽다): https://github.com/gabr42/GpDelphiUnits

 

알고리즘 향상에 관심이 있는 개발자에게 권장하는 자료


 

번호 제목 글쓴이 날짜 조회 수
공지 [DelphiCon 요약] 코드사이트 로깅 실전 활용 기법 (Real-world CodeSite Logging Techniques) 관리자 2021.01.19 22591
공지 [UX Summit 요약] 오른쪽 클릭은 옳다 (Right Click is Right) 관리자 2020.11.16 21024
공지 [10.4 시드니] What's NEW! 신기능 자세히 보기 관리자 2020.05.27 23082
공지 RAD스튜디오(델파이,C++빌더) - 고객 사례 목록 관리자 2018.10.23 28880
공지 [데브기어 컨설팅] 모바일 앱 & 업그레이드 마이그레이션 [1] 관리자 2017.02.06 30050
공지 [전체 목록] 이 달의 기술자료 & 기술레터 관리자 2017.02.06 25397
공지 RAD스튜디오(델파이, C++빌더) - 시작하기 [1] 관리자 2015.06.30 46347
공지 RAD스튜디오(델파이,C++빌더) - 모바일 앱 개발 사례 (2020년 11월 업데이트 됨) 험프리 2014.01.16 182316
1397 N 윈도우와 맥 개발 시작을 위한 파이어몽키 코스북: 무료 다운로드 제공(385페이지) 관리자 2013.04.05 152367
1396 ComPort(시리얼 통신) 컴포넌트 설치안내 [11] file 험프리 2013.12.04 112779
1395 [REST API] REST 기반 파일 업로드와 다운로드 구현하기 험프리 2020.08.31 84735
1394 델파이 튜토리얼 자습서 이용 안내 관리자 2014.09.01 71988
1393 이 달의 기술자료 - 2014년 11월 험프리 2014.10.13 54176
1392 이 달의 기술자료 - 2014년 6월 file 험프리 2014.06.05 50404
1391 Find the O/S Language Type c2design 2014.07.30 48421
1390 RAD Studio Resource Center 박병일 2012.01.26 46644
1389 CD-ROM 열고 닫기 박병일 2011.12.22 44787
1388 [Android] 폰번호 가져오기 [1] 타락천사 2014.09.05 38644
1387 이 달의 기술자료 - 2014년 12월 file 험프리 2014.11.26 32514
1386 RAD Studio XE6 Update1 발표 [1] Humphery 2014.06.20 29499
1385 델파이XE2 파이어몽키 기반 아이폰앱 개발에서 제스춰를 인식시키는 방법 박병일 2012.01.25 23342
1384 [10.4 시드니 신기능] 새로운 VCL TEdgeBrowser 컴포넌트 험프리 2020.05.18 23197