[DelphiCon 요약] 델파이 고성능 구현 (High Performance Delphi)
- 2020-12-29
- Posted by: Narae Kim
- Category: 기술자료
- 원본 비디오 시청: https://delphicon.embarcadero.com/talks/high-performance-delphi/
- DelphiCon 전체 보기 (현재 무료, 향후 유료 전환 예상): https://delphicon.embarcadero.com/replays/
- 데브기어의 DelphiCon 소개 페이지로 가기: https://devgear.co.kr/archives/3692
델파이 고성능 구현 (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
성능 향상 개요
’성능 향상’이 의미하는 바는 사람들마다 다를 수 있다. 하지만, ‘성능 향상 작업’을 하는 순서는 다음과 같다.
- 문제 식별 (‘측정’이 중요하다!)
- TStopwatch, 성능 카운터, GetTickCount 등을 활용하여 명시적으로 측정하자.
- AQTime 등 전문 프로파일링 도구를 사용하는 것도 좋다.
- ‘알고리즘 향상’: 가장 좋은 방법!!! (알고리즘 향상이 쉽지 않은 경우, 아래의 방법도 검토하자)
- 세부적인 코드 튜닝
- 병렬 처리 적용
- 외부 라이브러리 사용
- 더 좋은 알고리즘을 제공하는 라이브러리를 도입하여 빠르게 문제 해결 가능.
- 주의할 점: 외부 라이브러리 제작자가 유지보수를 하지 않는 상황에 대한 대비가 필요
- 어셈블러와 같은 저수준 언어로 (성능 향상이 필요한 부분을) 대체
- 주의할 점: 10년 후에는 지금보다도 저수준 언어 개발자 찾기가 더 어려워 진다는 점을 고려해야 함
알고리즘 향상
이 세션에서는 유용한 2가지를 예제와 함께 살펴본다.
- ‘더 좋은 알고리즘 적용’
- ‘불필요한 코드 실행 줄이기와 없애기
알고리즘 복잡도
- 데이터가 증가할 수록 프로그램이 어떻게 느려질 것인지를 판단할 때에도 유용하다.
- 프로그램의 성능 문제는 데이터가 많아지면서 발생하는 경우가 많다.
*관련 웹페이지 추천:
- http://bigocheatsheet.com: 알고리즘 별 복잡도가 잘 정리되어 있다.
- https://geeksforgeeks.org/data-structures: 다양한 데이터 구조에 대한 좋고 긴 토론이 정리되어 있다.
O(1)은 데이터가 아무리 증가해도 성능이 저하되지 않는다. 피보나치 수열 재귀호출은 급적하게 저하된다 (역자 주: 재귀 호출은 매우 강력하지만, 코딩 시 매우 주의해야 합니다. 이 세션에서도 재귀 호출과 관련된 예시와 개선 방향이 제시됩니다)
더 좋은 알고리즘 예제: 무작위 단어 검색 프로그램의 성능을 향상해보자
알고리즘 | 1초 이내 결과 | 1초 이상 결과 | 시간 초과 | 비고 |
(무작위 단어를 만들고) 정렬되지 않은 TStringList 검색 |
~ 4글자 | 5글자 | 6글자 |
|
(무작위 단어를 만들고) 정렬된 TStringList 검색 |
~ 6글자 | 7글자 | 8글자 |
|
(무작위 단어를 만들고) TDictionary 사용 |
~ 7글자 | 8글자 | 9글자 | |
(무작위 단어 조차 만들지 않고) 글자 수별로 TStringList를 따로 만들어서 사용 |
거의 무제한 | 해당 없음 | 해당 없음 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
<strong>// 정렬되지 않은 TStringList 검색 </strong> FindGoodWord ( function (const word: string): boolean begin Result := <strong>FWordsUnsorted</strong>.IndexOf(word) >= 0; end); <strong>// 정렬된 TStringList 검색 </strong> FindGoodWord ( function (const word: string): boolean begin Result := <strong>FWordsSorted</strong>.IndexOf(word) >= 0; end); <strong>//TDictionary 검색 </strong> FindGoodWord ( function (const word: string): boolean begin Result := <strong>FWordsDictionary.ContainsKey</strong>(word) >= 0; end); <strong>//글자 수별로 TStringList를 따로 만들고, 이 TStringList를 다시 오브젝트 리스트에 모아두고 사용하기 </strong> 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])); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
//참고: wordList는 파일에서 읽어서 만든 TStringList이다. <strong>// 정렬되지 않은 TStringList 생성 </strong> FWordsUnsorted := TStringList.Create; FWordsUnsorted.Assigned(wordList); <strong>// 정렬된 TStringList 생성 </strong> FWordsSorted := TStringList.Create; FWordsSorted.Assigned(wordList); FWordsSorted.Sorted := True; <strong>//TDictionary 생성 </strong> FWordsDictionary := TDictionary.Create(wordList.Count); for word in wordList do FWordsDictionary.Add(word, True); //Key에 단어를 넣고, Value는 사용하지 않으므로 True로 모두 넣는다. <strong>//글자 수별로 TStringList를 따로 만들고, 이 TStringList를 다시 오브젝트 리스트에 모아두고 사용하기 </strong> 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 단위로 읽고, 진행율을 표시하는 프로그램
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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;<strong> //문제 지점: 2백만번이나 화면을 업데이트하고 있었다 (이 부분만 주석처리하고 실행하면 바로 알 수 있다) </strong> end; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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); <strong> currPct := Round(total / CFileSize * 100);</strong> //백분율을 미리 계산하고 <strong>if currPct > lastPct then </strong>//1% 단위로만 (총 100번) 화면 업데이트 <strong>begin lastPct := currPct; ProgressBar1.Position := currPct; ProgressBar1.Update; end; </strong> end; |
불필요한 코드 실행 줄이기 예제: TListBox와 TMemo에 각각 10,000 개 항목을 넣는 프로그램
(참고:화면에 항목 하나를 표시하는 것은 그만큼 프로그램이 윈도우 운영체제와 메시지를 주고 받는다는 의미이다.
코드 | TListBox 실행 시간 | TMemo 실행 시간 |
루프 안에서 콘트롤에 10,000개 항목 추가 | 7초 | 28초 |
루프 앞뒤에 BeginUpdate와 EndUpdate 호출 | 0.5초 | 3초 |
루프 안에서는 화면이 없는 중간 매체(TStringList 사용)에 항목을 넣고, 루프 밖에서 TMemo에 한번에 넣기 | (해당 없음) | 0.2초 |
가상리스트 박스 (이것은 불필요한 코드 실행 없애기에서 따로 설명) | 0.003초 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<strong>// 루프 앞뒤에 BeginUpdate와 EndUpdate 호출 코드 (3초로 개선됨) </strong> Memo1.Lines.BeginUpdate; for i := 1 to CNumLines do Memo1.Lines.Add('Line ' + IntToStr(i)); Memo1.Lines.EndUpdate; <strong>// 루프 안에서는 TStringList에 항목을 넣고, 루프 밖에서 TMemo에 한번에 넣는 코드 (0.2초로 개선됨) </strong> 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: (리스트박스에 오브젝트가 들어가 있는 경우 사용한다. 지금은 생략)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// 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; <strong>//마찬가지로 (TListbox).Items.IndexOf([찾고자 하는 값]) 코드가 사용되는 경우를 대비하여 //데이터를 실제 가지고 있는 TStringList의 IndexOf 결과를 반환하도록 코드를 쓴다. //리스트박스는 실제로 항목을 가지고 있지 않다는 점을 명심하자 </strong> 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
알고리즘 향상에 관심이 있는 개발자에게 권장하는 자료
- 도서
- Julian Bucknall, Algorithms and Data Structures
- PrimožGabrijelčič, DelphiHighPerformance
- RobertSedgewick&KevinWayne, Algorithms
- 웹
- Algorithms, 4th Edition, https://algs4.cs.princeton.edu/home/
- Udacity,edX,Udemy,Coursera…
- https:www.geeksforgeeks.org/data-structures/
- https:www.geeksforgeeks.org/fundamentals-of-algorithms
- 포럼
- Delphi-PRAXIS, www.delphipraxis.net,en.delphipraxis.net
- 원본 비디오 시청: https://delphicon.embarcadero.com/talks/high-performance-delphi/
- DelphiCon 전체 보기 (현재 무료, 향후 유료 전환 예상): https://delphicon.embarcadero.com/replays/
- 데브기어의 DelphiCon 소개 페이지로 가기: https://devgear.co.kr/archives/3692
12.0 12.1 AI AWS C++ c++빌더 chatgpt DelphiCon ios rad서버 RAD스튜디오 UI UIUX UX uxsummit vcl 개발 개발사례 고객사례 기술레터 기술백서 데브옵스 데이터 데이터베이스 델파이 리눅스 마이그레이션 맥 머신러닝 모바일 새버전 샘플 세미나 안드로이드 웹 윈도우 인공지능 인터베이스 출시 커뮤니티에디션 코드 클라우드 파이썬 파이어몽키 현대화