원래 질문
왜 하나의 루프가 두 개의 루프보다 훨씬 느립니까?
결론:
사례 1 은 비효율적 인 전형적인 보간 문제입니다. 또한 이것이 많은 기계 아키텍처와 개발자가 멀티 스레드 응용 프로그램과 병렬 프로그래밍을 수행 할 수있는 멀티 코어 시스템을 구축하고 설계하게 된 주요 이유 중 하나라고 생각합니다.
RAM, 캐시, 페이지 파일 등을 사용하는 힙 할당을 수행하기 위해 하드웨어, OS 및 컴파일러가 함께 작동하는 방식을 포함하지 않고 이러한 종류의 접근 방식에서이를 살펴보십시오. 이 알고리즘의 기초가되는 수학은이 두 가지 중 어느 것이 더 나은 솔루션인지 보여줍니다.
우리는 노동자 와 사이를 여행해야하는 것을 나타내는 Boss
존재 의 비유를 사용할 수 있습니다 .Summation
For Loop
A
B
여행하는 데 필요한 거리와 작업자 간 소요 시간의 차이로 인해 사례 1 보다 약간 크지 않은 경우 사례 2 가 절반 이상으로 빠름을 쉽게 알 수 있습니다 . 이 수학은 BenchMark Times 및 조립 지침의 차이 수와 거의 사실상 완벽하게 일치합니다.
이제이 모든 것이 아래에서 어떻게 작동하는지 설명하겠습니다.
문제 평가
OP의 코드 :
const int n=100000;
for(int j=0;j<n;j++){
a1[j] += b1[j];
c1[j] += d1[j];
}
과
for(int j=0;j<n;j++){
a1[j] += b1[j];
}
for(int j=0;j<n;j++){
c1[j] += d1[j];
}
고려 사항
for 루프의 두 가지 변형에 대한 OP의 원래 질문과 캐시의 동작에 대한 수정 된 질문과 다른 많은 훌륭한 답변과 유용한 주석을 고려합니다. 이 상황과 문제에 대해 다른 접근법을 취하여 여기에서 다른 것을 시도하고 싶습니다.
접근
두 개의 루프와 캐시 및 페이지 정리에 대한 모든 토론을 고려할 때 다른 관점에서이를 보는 또 다른 접근법을 원합니다. 캐시와 페이지 파일을 포함하지 않는 메모리 나 메모리 할당을위한 실행은 실제로이 접근 방식은 실제 하드웨어 나 소프트웨어와 전혀 관련이 없습니다.
관점
코드를 잠시 살펴본 후 문제가 무엇인지, 코드가 생성되는 것이 분명해졌습니다. 이것을 알고리즘 문제로 나누고 수학 표기법을 사용하는 관점에서 살펴본 다음 수학 문제뿐만 아니라 알고리즘에도 적용 해 봅시다.
우리가 아는 것
우리는이 루프가 100,000 번 실행된다는 것을 알고 있습니다. 우리는 또한 알고 a1
, b1
, c1
및 d1
64 비트 아키텍처에 대한 포인터입니다. 32 비트 시스템의 C ++에서 모든 포인터는 4 바이트이고 64 비트 시스템에서는 포인터의 길이가 고정되어 있으므로 8 바이트 크기입니다.
우리는 두 경우 모두에 할당 할 32 바이트가 있다는 것을 알고 있습니다. 유일한 차이점은 각 반복마다 32 바이트 또는 2-8 바이트의 2 세트를 할당하는 것입니다. 두 번째 경우에는 두 개의 독립 루프 모두에 대해 각 반복마다 16 바이트를 할당합니다.
두 루프는 여전히 총 할당에서 32 바이트와 같습니다. 이 정보를 통해 이제 이러한 개념의 일반적인 수학, 알고리즘 및 유추를 보여 드리겠습니다.
우리는 두 경우 모두 동일한 집합 또는 작업 그룹이 수행해야하는 횟수를 알고 있습니다. 두 경우 모두 할당해야 할 메모리 양을 알고 있습니다. 두 경우 사이에 할당의 전체 워크로드가 거의 동일하다고 평가할 수 있습니다.
우리가 모르는 것
카운터를 설정하고 벤치 마크 테스트를 실행하지 않으면 각 사례에 소요되는 시간을 알 수 없습니다. 그러나 벤치 마크는 원래 질문과 일부 답변과 의견에서 이미 포함되었습니다. 우리는이 둘 사이에 큰 차이가 있음을 알 수 있습니다. 이것이이 문제에 대한이 제안에 대한 전체 추론입니다.
조사합시다
힙 할당, 벤치 마크 테스트, RAM, 캐시 및 페이지 파일을 보면 많은 사람들이 이미이 작업을 수행했음을 분명히 알 수 있습니다. 특정 데이터 포인트와 특정 반복 지수를 살펴 보았으며이 특정 문제에 대한 다양한 대화를 통해 많은 사람들이 다른 관련 문제에 대해 질문하기 시작했습니다. 수학적 알고리즘을 사용하고 유추를 적용하여이 문제를 어떻게 살펴볼 수 있습니까? 우리는 몇 가지 주장을 시작하여 시작합니다! 그런 다음 알고리즘을 구축합니다.
우리의 주장 :
- 루프에서와 같이 루프와 반복은 0에서 시작하는 대신 1에서 시작하여 100000에서 끝나는 Summation이 될 것입니다. 알고리즘 자체.
- 두 경우 모두 작업 할 4 개의 함수와 각 함수 호출마다 2 개의 작업이 수행되는 2 개의 함수 호출이 있습니다. 우리는 다음과 같은 기능에 대한 기능과 전화 등이 최대를 설정합니다 :
F1()
, F2()
, f(a)
, f(b)
, f(c)
와 f(d)
.
알고리즘 :
첫 번째 경우 : -하나의 요약 만 있고 두 개의 독립적 인 함수 호출.
Sum n=1 : [1,100000] = F1(), F2();
F1() = { f(a) = f(a) + f(b); }
F2() = { f(c) = f(c) + f(d); }
두 번째 경우 : -두 개의 요약이 있지만 각각 고유 한 함수 호출이 있습니다.
Sum1 n=1 : [1,100000] = F1();
F1() = { f(a) = f(a) + f(b); }
Sum2 n=1 : [1,100000] = F1();
F1() = { f(c) = f(c) + f(d); }
당신이 발견하는 경우 F2()
에만 존재 Sum
에서 Case1
경우 F1()
에 포함 Sum
에서 Case1
모두에서 Sum1
와 Sum2
에서 Case2
. 이것은 나중에 두 번째 알고리즘 내에서 최적화가 있다고 결론을 내릴 때 분명해질 것입니다.
자체에 추가 되는 첫 번째 경우 Sum
호출 f(a)
을 통한 반복 은 동일하지만 각 반복 에 대해 자체적으로 추가 되는 f(b)
호출 f(c)
입니다 . 두 번째 경우에, 우리는이 과 가 같은 기능을 두 번 연속 호출되는 것처럼 모두 같은 행동 것이다.f(d)
100000
Sum1
Sum2
이 경우 우리는 처리 할 수 Sum1
및 Sum2
다만 보통 오래된 같은 Sum
경우 Sum
이 경우이 같은 모습에 : Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }
지금 우리가 그냥 같은 기능으로 간주 할 수있는 최적화 등이 보인다.
유추로 요약
두 번째 경우에서 보았 듯이 두 for 루프 모두 동일한 정확한 서명을 갖기 때문에 최적화가있는 것처럼 거의 나타나지만 실제 문제는 아닙니다. 문제는에 의해 수행되고있는 작업 아니다 f(a)
, f(b)
, f(c)
,와 f(d)
. 두 경우와 둘 사이의 비교에서, 각 경우에 Summation이 이동해야하는 거리의 차이가 실행 시간의 차이를 제공합니다.
의 생각 For Loops
것으로 Summations
수있는 Being으로 반복 수행이 Boss
이명에 명령을 내리고되는 A
&를 B
자신의 작업 고기 것을 C
및 D
각각 그들에서 일부 패키지를 선택하고 그것을 반환 할 수 있습니다. 이 비유에서, for 루프 또는 합산 반복 및 조건 확인 자체는 실제로를 나타내지 않습니다 Boss
. 어떤 사실이 나타내는 것은 Boss
직접 실제의 수학적 알고리즘에서하지만 실제 개념으로부터 아니다 Scope
및 Code Block
루틴 또는 서브 루틴, 메소드, 함수, 변환 부 등을 제 알고리즘은 제 2 알고리즘은 2 개 개의 연속 범위를 갖는 1 개 범위가 내.
각 호출 전표의 첫 번째 사례 내에서 Boss
이동 A
은 주문을 전달하고 패키지 A
를 가져 오기 B's
위해 Boss
이동 한 다음 C
주문은 동일한 작업을 수행하고 D
각 반복 에서 패키지를 수신하도록 명령합니다 .
두 번째 경우에는 모든 패키지가 수신 될 때까지 패키지 를 Boss
직접 A
가져오고 가져 오기 위해 직접 작업합니다 B's
. 그런 다음 모든 패키지 를 가져 오기 위해 동일한 Boss
작업 C
을 수행 D's
합니다.
우리는 8 바이트 포인터로 작업하고 힙 할당을 다루기 때문에 다음 문제를 고려해 봅시다. 하자이 (가) 말 Boss
1백피트 출신 A
과 그 A
에서 500 피트입니다 C
. 실행 순서로 인해 Boss
처음부터 얼마나 멀리 떨어져 있는지 걱정할 필요가 없습니다 C
. 두 경우 모두 Boss
처음에는 처음부터 A
다음으로 이동 합니다 B
. 이 비유는이 거리가 정확하다고 말하는 것은 아닙니다. 알고리즘의 작동을 보여주는 유용한 테스트 사례 시나리오 일뿐입니다.
대부분의 경우 힙 할당을 수행하고 캐시 및 페이지 파일로 작업 할 때 주소 위치 간의 거리는 그다지 다르지 않거나 데이터 유형의 특성 및 배열 크기에 따라 크게 달라질 수 있습니다.
테스트 사례 :
첫 번째 사례 : 첫 번째 반복에서는Boss
처음에 주문 슬립을주고 100 발을 가야A
하고A
꺼지고 자신의 일을하지만, 다음은Boss
500 발을 여행하는C
그에게 자신의 주문 전표를 제공 할 수 있습니다. 그런 다음 다음 반복과 다른 모든 반복Boss
에서 두 사이에서 500 피트를 앞뒤로 이동해야합니다.
두 번째 사례 : 는Boss
에 첫 번째 반복 100 발을 여행하는A
, 그러나 그 후, 그는 이미이 있고만을위한 대기A
모든 전표가 작성 될 때까지 다시 얻을. 그 다음은Boss
에 첫 번째 반복에 5백피트 여행이C
때문에C
500 피트입니다A
. 이것이Boss( Summation, For Loop )
작업 후 바로 호출되기 때문에모든주문 전표가 완료될 때까지A
그가했던 것처럼 기다립니다. A
C's
이동 거리의 차이
const n = 100000
distTraveledOfFirst = (100 + 500) + ((n-1)*(500 + 500);
// Simplify
distTraveledOfFirst = 600 + (99999*100);
distTraveledOfFirst = 600 + 9999900;
distTraveledOfFirst = 10000500;
// Distance Traveled On First Algorithm = 10,000,500ft
distTraveledOfSecond = 100 + 500 = 600;
// Distance Traveled On Second Algorithm = 600ft;
임의의 가치 비교
600이 천만보다 훨씬 적다는 것을 쉽게 알 수 있습니다. RAM의 주소 또는 각 반복마다 호출 할 캐시 또는 페이지 파일 간의 거리 차이에 대한 실제 차이를 알 수 없기 때문에 이것은 정확하지 않습니다. 이것은 최악의 시나리오에서 상황을 인식하고보고있는 상황에 대한 평가 일뿐입니다.
이 숫자들에서 거의 알고리즘 1 99%
이 알고리즘 2보다 느려 야하는 것처럼 보입니다 . 그러나, 이것은 단지입니다 Boss's
일부 또는 알고리즘의 책임과 실제 노동자 고려하지 않습니다 A
, B
, C
, D
그리고 그들이 각각의 루프의 모든 반복에해야한다. 따라서 보스의 업무는 총 작업 중 약 15-40 % 만 차지합니다. 작업자를 통해 수행되는 대부분의 작업은 속도 차이의 비율을 약 50-70 %로 유지하는 데 약간 더 큰 영향을 미칩니다.
관찰 : - 두 알고리즘의 차이점
이 상황에서 그것은 수행되는 작업의 프로세스 구조입니다. 사례 2 는 이름과 거리에 따라 다른 변수 만있는 유사한 함수 선언 및 정의를 갖는 부분 최적화 모두에서 더 효율적 임을 보여줍니다 .
우리는 또한 사례 1 에서 이동 한 총 거리 가 사례 2 에서보다 훨씬 먼 거리를 보았으며이 거리 가 두 알고리즘 사이 의 시간 계수로 이동 한 것으로 간주 할 수 있습니다 . 사례 1 은 사례 2 보다 훨씬 더 많은 작업을 수행 합니다.
이는 ASM
두 경우 모두에 표시된 지침 의 증거 에서 확인할 수 있습니다. 이미 이러한 경우에 대해 언급 한 것과 함께,이에 있다는 사실을 고려하지 않는 경우 1 보스가 모두 기다려야 할 것 A
& C
그가 다시 갈 전에 돌아 가야 A
각 반복에 대해 다시. 또한 시간이 오래 걸리 A
거나 B
너무 오래 걸리면 Boss
다른 작업자와 다른 작업자가 모두 실행 대기 중이 라는 사실을 고려하지 않습니다 .
에서 사례 2 유일한 존재 유휴은은 Boss
노동자가 돌아 오기까지. 따라서 이것도 알고리즘에 영향을 미칩니다.
OP 수정 된 질문
편집 : 문제는 배열의 크기 (n)와 CPU 캐시에 따라 크게 다르기 때문에 질문은 관련이없는 것으로 판명되었습니다. 따라서 더 관심이 있다면 질문을 다시 표현하십시오.
다음 그래프의 5 개 영역에서 설명하는 것처럼 다른 캐시 동작을 유발하는 세부 사항에 대한 통찰력을 제공 할 수 있습니까?
이러한 CPU에 대해 유사한 그래프를 제공하여 CPU / 캐시 아키텍처의 차이점을 지적하는 것도 흥미로울 수 있습니다.
이 질문들에 대하여
의심 할 여지없이 시연 한 바와 같이 하드웨어 및 소프트웨어가 관여되기 전에도 근본적인 문제가 있습니다.
이제 페이지 파일 등과 함께 메모리 및 캐싱을 관리 할 때 다음과 같은 통합 시스템 세트에서 모두 작동합니다.
The Architecture
{하드웨어, 펌웨어, 일부 임베디드 드라이버, 커널 및 ASM 명령어 세트}.
The OS
{파일 및 메모리 관리 시스템, 드라이버 및 레지스트리}.
The Compiler
{소스 코드의 번역 단위 및 최적화}.
- 또한
Source Code
고유 알고리즘 세트가 있는 자체 도 마찬가지 입니다.
우리는 이미 우리가 심지어 임의의 어떤 시스템에 적용하기 전에 먼저 알고리즘 내에서 일어나는 병목 현상이 있음을 볼 수 있습니다 Architecture
, OS
그리고 Programmable Language
두 번째 알고리즘에 비해. 현대 컴퓨터의 본질을 다루기 전에 이미 문제가있었습니다.
결말 결과
하나; 이 새로운 질문들이 그들 자신이기 때문에 중요하지 않다고 말할 수는 없습니다. 그것들은 절차와 전반적인 성과에 영향을 미치며, 답변이나 의견을 제시 한 많은 사람들의 다양한 그래프와 평가를 통해 알 수 있습니다.
당신의 비유에 주목하면 Boss
두 노동자 A
및 B
이동에서 패키지를 검색했다 C
및 D
각각 문제의 두 알고리즘의 수학적 표기 고려; 당신은 컴퓨터 하드웨어의 참여없이 볼 수 있으며 소프트웨어는 Case 2
약 60%
보다 더 빨리 Case 1
.
이러한 알고리즘을 일부 소스 코드에 적용하고, 컴파일, 최적화 및 OS를 통해 실행하여 주어진 하드웨어에서 작업을 수행 한 후 그래프와 차트를 보면 차이 사이에서 약간의 성능 저하를 볼 수 있습니다 이 알고리즘에서.
는 IF Data
세트가 상당히 작습니다 그것은 처음에 차이의 모든 나쁜 보이지 않을 수 있습니다. 그러나 이후로는 Case 1
에 관한 60 - 70%
보다 느리게 Case 2
우리가 시간 실행의 차이의 관점에서이 함수의 성장을 볼 수 있습니다 :
DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where
Loop1(time) = Loop2(time) + (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with
DeltaTimeDifference approximately = (Loop2(time) + (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)
이 근사치는 알고리즘과 소프트웨어 최적화 및 기계 명령어가 포함 된 기계 작동 모두에서이 두 루프의 평균 차이입니다.
데이터 세트가 선형으로 증가하면 둘 사이의 시간 차이도 커집니다. 알고리즘 1은 알고리즘 2보다 더 많은 인출 (fetch)을 가지고 있는데, 알고리즘 2 는 첫 번째 반복 후 모든 반복에 대해 & Boss
사이의 최대 거리를 앞뒤로 이동 해야하는 반면, 알고리즘 2는 한 번만 이동 한 다음 이동 이 완료된 후 분명 해야합니다. 한 번만 최대 거리에서 갈 때 까지 .A
C
Boss
A
A
A
C
Boss
비슷한 연속 작업에 집중하는 대신 두 가지 유사한 일을 한 번에 수행하고 앞뒤로 저글링 하는 데 집중 하려고 노력하는 것은 여행과 업무를 두 배로 늘려야했기 때문에 하루가 끝날 무렵 그를 화나게 할 것입니다. 따라서 상사의 배우자와 자녀가 그것을 인정하지 않기 때문에 상사가 보간 된 병목에 빠지게하여 상황의 범위를 잃지 마십시오.
개정 : 소프트웨어 엔지니어링 설계 원칙
- 반복 for 루프 내에서의 계산 의 차이 Local Stack
와 Heap Allocated
사용법, 효율성 및 효율성의 차이-
위에서 제안한 수학적 알고리즘은 주로 힙에 할당 된 데이터에 대한 작업을 수행하는 루프에 적용됩니다.
- 연속 스택 작업 :
- 루프가 스택 프레임 내에있는 단일 코드 블록 또는 범위 내에서 로컬로 데이터에 대한 작업을 수행하는 경우 여전히 적용되지만 메모리 위치는 일반적으로 순차적 인 위치와 이동 거리 또는 실행 시간의 차이에 훨씬 더 가깝습니다. 거의 무시할 수 있습니다. 힙 내에서 할당이 수행되지 않기 때문에 메모리가 분산되지 않으며 메모리가 램을 통해 페치되지 않습니다. 메모리는 일반적으로 순차적이며 스택 프레임 및 스택 포인터에 상대적입니다.
- 스택에서 연속 작업이 수행되는 경우 최신 프로세서는 반복 값과 주소를 캐시하여 이러한 값을 로컬 캐시 레지스터 내에 유지합니다. 여기에서 작동 또는 명령 시간은 나노초 단위입니다.
- 연속 힙 할당 작업 :
- 힙 할당을 적용하기 시작하고 프로세서가 CPU, 버스 컨트롤러 및 Ram 모듈의 아키텍처에 따라 연속 호출에서 메모리 주소를 가져와야 할 때 작업 또는 실행 시간은 마이크로에서 밀리 초 캐시 된 스택 작업과 비교하면 속도가 느립니다.
- CPU는 Ram에서 메모리 주소를 가져와야하며 일반적으로 시스템 버스의 모든 내용은 CPU 자체의 내부 데이터 경로 또는 데이터 버스와 비교할 때 느립니다.
따라서 힙에 있어야하는 데이터로 작업하고 루프를 통해 순회하는 경우 각 데이터 세트와 해당 알고리즘을 자체 단일 루프 내에 유지하는 것이 더 효율적입니다. 힙에있는 서로 다른 데이터 세트의 여러 작업을 단일 루프에 배치하여 연속 루프를 제거하는 것보다 더 나은 최적화를 얻을 수 있습니다.
자주 캐시되기 때문에 스택에있는 데이터를 사용하여이 작업을 수행 할 수 있지만 메모리 주소가 모든 반복을 쿼리해야하는 데이터에는 적용되지 않습니다.
소프트웨어 엔지니어링과 소프트웨어 아키텍처 디자인이 시작됩니다. 데이터를 구성하는 방법, 데이터를 캐시 할시기, 힙에 데이터를 할당 할시기, 알고리즘을 설계 및 구현하는 방법, 알고리즘을 호출하는시기 및 위치를 알 수있는 기능입니다.
동일한 데이터 세트와 관련된 동일한 알고리즘을 가질 수 있지만 O(n)
작업시 알고리즘 의 복잡성으로 인한 위의 문제 때문에 스택 변형에 대한 구현 설계와 힙 할당 변형에 대한 구현 설계를 원할 수 있습니다. 힙과 함께.
몇 년 동안 내가 알았던 것에서 많은 사람들이이 사실을 고려하지 않습니다. 이들은 특정 데이터 세트에서 작동하는 하나의 알고리즘을 설계하는 경향이 있으며 스택에서 로컬로 캐시되는 데이터 세트 또는 힙에 할당 된 데이터 세트에 관계없이 알고리즘을 사용합니다.
진정한 최적화를 원한다면 코드 복제처럼 보일 수 있지만 일반화하는 경우 동일한 알고리즘의 두 가지 변형을 갖는 것이 더 효율적입니다. 하나는 스택 작업을위한 것이고 다른 하나는 반복 루프에서 수행되는 힙 작업을위한 것입니다!
여기에 의사 예제가 있습니다 : 두 개의 간단한 구조체, 하나의 알고리즘.
struct A {
int data;
A() : data{0}{}
A(int a) : data{a}{}
};
struct B {
int data;
B() : data{0}{}
A(int b) : data{b}{}
}
template<typename T>
void Foo( T& t ) {
// do something with t
}
// some looping operation: first stack then heap.
// stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};
// For stack operations this is okay and efficient
for (int i = 0; i < 10; i++ ) {
Foo(dataSetA[i]);
Foo(dataSetB[i]);
}
// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i++ ) {
Foo(dataSetA[i]); // dataSetA is on the heap here
Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.
// To improve the efficiency above, put them into separate loops...
for (int i = 0; i < 10; i++ ) {
Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i++ ) {
Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only psuedo code
// to illustrate a point.
이것이 스택 변형과 힙 변형에 대해 별도의 구현을 통해 언급 한 것입니다. 알고리즘 자체는 그다지 중요하지 않습니다. 그것은 여러분이 사용할 루프 구조입니다.