[performance] 최후의 수단의 성능 최적화 전략 [닫기]

이 사이트에는 이미 성능에 관한 질문이 많이 있지만, 거의 모든 것이 문제별로 다르고 상당히 좁습니다. 거의 모든 것이 조기 최적화를 피하기 위해 조언을 반복합니다.

가정하자 :

  • 코드가 이미 올바르게 작동하고 있습니다
  • 선택한 알고리즘은 문제의 상황에 대해 이미 최적입니다
  • 코드가 측정되었으며 문제가되는 루틴이 격리되었습니다.
  • 모든 최적화 시도도 문제를 악화시키지 않도록 측정됩니다.

내가 여기서 찾고있는 것은 남은 일 외에는 아무것도하지 않을 때 중요한 알고리즘에서 마지막 몇 퍼센트까지 짜내는 전략과 요령입니다.

이상적으로는 언어에 구애받지 않고 대답을 시도하고 해당되는 경우 제안 된 전략에 대한 단점을 나타냅니다.

나는 자신의 초기 제안으로 답장을 추가하고 스택 오버플로 커뮤니티가 생각할 수있는 다른 것을 기대합니다.



답변

좋아, 당신은 개선의 여지가별로없는 것처럼 문제를 정의하고 있습니다. 내 경험상 그것은 매우 드 rare니다. 나는 1993 년 11 월 Dr. Dobbs 기사에서 명백한 낭비가없는 기존의 잘 설계된 비 사소한 프로그램에서 시작하여 벽시계 시간이 48 초에서 단축 될 때까지 일련의 최적화를 통해 설명했습니다. 1.1 초로, 소스 코드 크기는 4 배 줄었습니다. 내 진단 도구 는 다음과 같습니다 . 변경 순서는 다음과 같습니다.

  • 발견 된 첫 번째 문제점은 목록 클러스터 (현재 “반복자”및 “컨테이너 클래스”)를 절반 이상 사용하는 것입니다. 그것들은 상당히 간단한 코드로 대체되어 시간이 20 초로 단축되었습니다.

  • 이제 가장 큰 타임 테이커는 더 많은 목록 작성입니다. 백분율로 보면 이전에는 그리 크지 않았지만 이제는 더 큰 문제가 제거 되었기 때문입니다. 속도를 높이는 방법을 찾으면 시간이 17 초로 줄어 듭니다.

  • 이제 명백한 범인을 찾기가 더 어렵지만, 내가 할 수있는 몇 가지 작은 것들이 있으며 시간은 13 초로 떨어집니다.

이제 벽에 부딪힌 것 같습니다. 샘플은 정확히 무엇을하고 있는지 알려주지 만 개선 할 수있는 것을 찾을 수없는 것 같습니다. 그런 다음 프로그램의 기본 디자인, 트랜잭션 중심 구조에 대해 생각하고 수행중인 모든 목록 검색이 실제로 문제의 요구 사항에 의해 지시되는지 묻습니다.

그런 다음 프로그램 코드가 더 작은 소스 세트에서 실제로 (전 처리기 매크로를 통해) 생성되고 프로그램이 프로그래머가 알고있는 것을 지속적으로 파악하지 못하는 재 설계에 착수했습니다. 다시 말해, 일련의 작업을 “해석”하지 말고 “컴파일”하십시오.

  • 재 설계가 완료되고 소스 코드가 4 배 줄어들고 시간이 10 초로 줄어 듭니다.

이제는 점점 빨라지기 때문에 샘플링하기가 어렵 기 때문에 10 배나 많은 작업을 수행하지만 다음 시간은 원래 작업량을 기준으로합니다.

  • 더 많은 진단은 대기열 관리에 시간을 보내고 있음을 나타냅니다. 인라인으로 시간을 7 초로 줄입니다.

  • 이제 큰 시간을 보냈다는 것은 내가하고 있던 진단 인쇄입니다. 플러시-4 초.

  • 이제 가장 큰 타임 테이커는 malloc을 호출 하고 무료 입니다. 개체 재활용-2.6 초

  • 계속해서 샘플을 보더라도 여전히 1.1 초라는 꼭 필요한 작업은 없습니다.

총 속도 향상 요소 : 43.6

이제 두 프로그램이 비슷하지는 않지만 장난감이 아닌 소프트웨어에서는 항상 이와 같은 진행 상황을 보았습니다. 먼저 수익이 줄어들 기 전까지는 쉬운 일을하고 더 어려운 일을합니다. 그렇다면 당신이 얻는 통찰력은 다시 디자인이 줄어들어 다시 줄어드는 수익에 도달 할 때까지 새로운 속도 향상을 시작합니다. 이제이이 있는지 궁금해에 적합 할 수 있습니다되는 지점입니다 ++i또는 i++또는 for(;;)또는 while(1)입니다 빨리 : 나는 스택 오버플로에 너무 자주 볼 질문의 종류.

PS 왜 프로파일 러를 사용하지 않았는지 궁금 할 것입니다. 이에 대한 답은 이러한 “문제”중 거의 모든 것이 샘플을 정확하게 찾아주는 함수 호출 사이트라는 것입니다. 오늘날에도 프로파일 러는 전체 함수보다 명령문 및 호출 명령이 위치를 찾고 수정하기가 더 중요하다는 생각을 거의 듣지 않고 있습니다.

실제로이 작업을 수행하기 위해 프로파일 러를 구축했지만 코드가 수행하는 작업에 대한 불길하고 친밀한 친밀감을 얻으려면 손가락을 바로 넣을 수 없습니다. 발견되는 문제가 너무 작아서 쉽게 놓칠 수 없기 때문에 샘플 수가 적다는 것은 문제가되지 않습니다.

추가 : jerryjvl이 몇 가지 예를 요청했습니다. 첫 번째 문제는 다음과 같습니다. 시간이 절반 이상 걸리는 소수의 개별 코드 줄로 구성됩니다.

 /* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)

이들은 목록 클러스터 ILST (목록 클래스와 유사)를 사용하고있었습니다. 그것들은 일반적인 방법으로 구현되며, “정보 숨기기”는 클래스 사용자가 구현 방식에 신경 쓸 필요가 없다는 것을 의미합니다. 이 줄이 작성되었을 때 (약 800 줄의 코드 중), 이것이 “병목 현상”(나는 그 단어를 싫어한다) 일 수 있다는 생각에 대해서는 생각하지 않았다. 그들은 단순히 일을하는 권장 방법입니다. 가급적이면 이러한 것들을 피해야한다고 말하기는 쉽지만 내 경험상 모든 성능 문제는 이와 같습니다. 일반적으로 성능 문제가 발생하지 않도록하는 것이 좋습니다. “피해야 했음”(후시)에도 생성 된 것을 찾고 수정하는 것이 더 좋습니다.

두 번째 문제는 두 줄로 나뉘어져 있습니다.

 /* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)

이들은 끝에 항목을 추가하여 건물 목록입니다. (수정은 항목을 배열로 수집하고 한 번에 목록을 작성하는 것이 었습니다.) 흥미로운 점은 이러한 문장이 원래 시간의 3/48에 불과하기 때문에 비용이 들지 않았다는 것입니다. 사실 처음 에는 큰 문제 입니다. 그러나 첫 번째 문제를 제거한 후 3/20의 시간이 소요되어 이제는 “더 큰 물고기”가되었습니다. 일반적으로 그렇게됩니다.

이 프로젝트가 내가 도와 준 실제 프로젝트에서 증류되었다고 덧붙일 수 있습니다. 이 프로젝트에서 내부 루프 내에서 데이터베이스 액세스 루틴을 호출하여 작업이 완료되었는지 확인하는 등 성능 문제가 훨씬 더 급격했습니다.

참고 ADDED : 소스 코드, 모두 원본과 재 설계는에서 찾을 수 있습니다 www.ddj.com 파일 9311.zip 파일 slug.asc 및 slug.zip에서, 1993.

2011/11/26 편집 : 이제 Visual C ++에 소스 코드가 포함 된 SourceForge 프로젝트 와 조정 방법에 대한 자세한 설명이 있습니다. 그것은 위에서 설명한 시나리오의 전반부를 통과하며 정확히 동일한 순서를 따르지 않지만 여전히 2-3 배의 속도 향상을 얻습니다.


답변

제안 :

  • 재 계산이 아닌 사전 계산 : 입력 범위가 상대적으로 제한된 계산이 포함 된 루프 또는 반복 호출의 경우 유효한 범위의 모든 값에 대한 계산 결과를 포함하는 조회 (배열 또는 사전)를 고려하십시오. 입력. 그런 다음 알고리즘 내부에서 간단한 조회를 사용하십시오.
    다운 측면 : 미리 계산 된 값의 몇 실제로이 문제를 악화시킬 수 있습니다 사용하는 경우, 또한 조회는 상당한 메모리를 취할 수있다.
  • 라이브러리 메소드를 사용하지 마십시오 . 대부분의 라이브러리는 광범위한 시나리오에서 올바르게 작동하고 매개 변수 등에서 널 점검을 수행해야합니다. 메소드를 다시 구현하면 많은 로직을 제거 할 수 있습니다. 사용하는 정확한 환경에는 적용되지 않습니다.
    단점 : 추가 코드를 작성하면 버그가 더 많이 나타납니다.
  • 도서관 방법을 사용하십시오 : 자신과 모순되기 때문에 언어 도서관은 당신이나 나보다 훨씬 똑똑한 사람들에 의해 작성됩니다. 그들이 더 빠르고 더 잘했을 것입니다. 실제로 더 빨리 만들 수 없다면 직접 구현하지 마십시오 (예 : 항상 측정!).
  • 치트 : 어떤 경우에는 문제에 대한 정확한 계산이 가능하지만 ‘정확한’필요가 없을 수도 있습니다. 때로는 근사치가 ‘충분히 좋으며’거래가 훨씬 빠를 수도 있습니다. 스스로 답하십시오. 답이 1 % 나쁘면 정말 중요합니까? 5 %? 심지어 10 %?
    단점 : 음 … 정답은 정확하지 않습니다.

답변

더 이상 성능을 향상시킬 수없는 경우 대신 인식 된 성능을 향상시킬 수 있는지 확인하십시오 .

fooCalc 알고리즘을 더 빨리 만들지 못할 수도 있지만 종종 응용 프로그램이 사용자에게보다 반응 적으로 보이도록하는 방법이 있습니다.

몇 가지 예 :

  • 사용자가 무엇을 요청할지 예상하고 그 전에 작업을 시작합니다.
  • 마지막에 한 번에 모든 결과가 표시되는 대신 결과 표시
  • 정확한 진행 측정기

이렇게하면 프로그램이 더 빨라지지는 않지만 속도가 빨라지면 사용자가 더 행복해질 수 있습니다.


답변

나는 대부분의 인생을이 곳에서 보냅니다. 광범위한 스트로크는 프로파일 러를 실행하고 기록하도록합니다.

  • 캐시가 누락되었습니다 . 데이터 캐시는 대부분의 프로그램에서 중단의 # 1 소스입니다. 더 나은 지역성을 갖도록 문제가되는 데이터 구조를 재구성하여 캐시 적중률을 향상시킵니다. 낭비되는 바이트 (따라서 캐시 페치 낭비)를 제거하기 위해 구조 및 숫자 유형을 압축합니다. 스톨을 줄이려면 가능한 한 데이터를 프리 페치하십시오.
  • 적중 상점 . 포인터 앨리어싱에 대한 컴파일러 가정 및 메모리를 통해 연결이 끊어진 레지스터 세트간에 데이터가 이동하는 경우 특정 병리학 적 동작으로 인해 전체 CPU 파이프 라인이로드 op에서 지워질 수 있습니다. 부동 소수점, 벡터 및 정수가 서로 캐스팅되는 장소를 찾아서 제거하십시오. __restrict앨리어싱에 대해 컴파일러에게 약속 하려면 자유로이 사용하십시오 .
  • 마이크로 코드 된 작업 . 대부분의 프로세서에는 파이프 라인 할 수없는 일부 작업이 있지만 대신 ROM에 저장된 작은 서브 루틴을 실행하십시오. PowerPC의 예는 정수 곱하기, 나누기 및 변수 별 시프트입니다. 문제는이 작업이 실행되는 동안 전체 파이프 라인이 중지되는 것입니다. 이러한 작업의 사용을 없애거나 최소한 파이프 라인 ops로 분류하여 나머지 프로그램이 수행하는 모든 작업에서 수퍼 스칼라 디스패치의 이점을 얻을 수 있습니다.
  • 지점이 잘못 예측 합니다. 파이프 라인이 비어 있습니다. CPU가 분기 후 파이프를 다시 채우는 데 많은 시간을 소비하는 사례를 찾고 가능한 경우 분기 힌트를 사용하여 더 자주 올바르게 예측하십시오. 또는 파이프가 일반적으로 더 깊고 fcmp 후에 조건 플래그를 읽으면 중단이 발생할 수 있으므로 특히 부동 소수점 연산 후에 분기를 조건부 이동으로 대체하십시오 .
  • 순차적 부동 소수점 연산 . 이 SIMD를 만드십시오.

그리고 내가하고 싶은 한 가지 더 :

  • 컴파일러에서 어셈블리 목록을 출력하도록 설정 하고 코드에서 핫스팟 함수에 대해 방출되는 것을 살펴보십시오. “좋은 컴파일러가 자동으로 당신을 위해 할 수 있어야한다”는 모든 영리한 최적화? 실제 컴파일러가 그렇게하지 않을 가능성이 있습니다. GCC가 실제로 WTF 코드를 생성하는 것을 보았습니다.

답변

더 많은 하드웨어를 던져라!


답변

더 많은 제안 :

  • I / O 피하기 : 모든 I / O (디스크, 네트워크, 포트 등)는 항상 계산을 수행하는 코드보다 훨씬 느리므로 반드시 필요하지 않은 I / O를 제거하십시오.

  • I / O를 사전에 이동 : 계산에 필요한 모든 데이터를 사전에로드하여 중요한 알고리즘의 핵심 내에서 I / O 대기를 반복하지 않도록 (결과가 반복 될 수 있음) 디스크 검색 (한 번의 히트로 모든 데이터를로드 할 때 검색을 피할 수 있음).

  • I / O 지연 : 계산이 끝날 때까지 결과를 쓰지 말고 데이터 구조에 저장 한 다음 열심히 일할 때 한 번에 그 결과를 덤프하십시오.

  • 스레드 I / O : 대담한 사람들을 위해,로드를 병렬 스레드로 이동하여 ‘I / O up-front’또는 ‘Delay I / O’를 실제 계산과 결합하여 더 많은 데이터를로드하는 동안 작업 할 수 있습니다 이미 보유한 데이터를 계산하거나 다음 데이터 배치를 계산하는 동안 마지막 배치의 결과를 동시에 쓸 수 있습니다.


답변

많은 성능 문제는 데이터베이스 문제와 관련되므로 쿼리 및 저장 프로 시저를 조정할 때 살펴볼 특정 사항을 알려 드리겠습니다.

대부분의 데이터베이스에서 커서를 사용하지 마십시오. 루핑도 피하십시오. 대부분의 경우 데이터 액세스는 레코드 처리에 의한 레코드가 아니라 설정 기반이어야합니다. 여기에는 1,000,000 개의 레코드를 한 번에 삽입하려는 경우 단일 레코드 저장 프로 시저를 재사용하지 않는 것이 포함됩니다.

select *를 사용하지 말고 실제로 필요한 필드 만 반환하십시오. 조인 필드가 반복되어 서버와 네트워크 모두에 불필요한로드가 발생하기 때문에 조인이있는 경우 특히 그렇습니다.

상관 된 하위 쿼리를 사용하지 마십시오. 조인 (가능한 경우 파생 테이블에 대한 조인 포함)을 사용하십시오 (Microsoft SQL Server에서는 이것이 사실이지만 다른 백엔드를 사용할 때 조언을 테스트하십시오).

인덱스, 인덱스, 인덱스 데이터베이스에 해당되는 경우 해당 통계를 업데이트하십시오.

쿼리를 sargable로 만듭니다 . 의미는 like 절의 첫 문자에 와일드 카드를 사용하거나 조인의 함수 또는 where 문의 왼쪽 부분에 색인을 사용할 수없는 것을 피하십시오.

올바른 데이터 유형을 사용하십시오. 문자열 데이터 유형을 날짜 데이터 유형으로 변환 한 다음 계산을 수행하는 것보다 날짜 필드에서 날짜 계산을 수행하는 것이 더 빠릅니다.

어떤 종류의 루프도 트리거에 넣지 마십시오!

대부분의 데이터베이스에는 쿼리 실행 방법을 확인할 수있는 방법이 있습니다. Microsoft SQL Server에서는이를 실행 계획이라고합니다. 문제 영역이 어디에 있는지 먼저 확인하십시오.

최적화해야 할 사항을 결정할 때 쿼리 실행 빈도와 실행 시간을 고려하십시오. 때때로 한 달에 한 번만 실행되는 long_running 쿼리에서 시간을 지우는 것보다 약간의 조정에서 하루에 수백만 번 실행되는 쿼리까지 더 많은 성능을 얻을 수 있습니다.

어떤 종류의 프로파일 러 도구를 사용하여 데이터베이스에서 실제로 전송되는 내용을 찾으십시오. 과거에 저장 프로 시저가 빠를 때 웹 페이지가 한 번이 아니라 여러 번 쿼리를 요청하는 프로파일 링을 통해 페이지가로드되는 속도가 느린 이유를 알 수 없었던 적이 있습니다.

프로파일 러는 누가 누가 차단하고 있는지 찾는 데 도움을줍니다. 혼자 실행하는 동안 빠르게 실행할 일부 쿼리로 인해 다른 쿼리의 잠금에 정말 느린 될 수 있습니다.