이 지연 루프가 수면없이 여러 번 반복 한 후 더 빨리 실행되기 시작하는 이유는 무엇입니까?
중히 여기다:
#include <time.h>
#include <unistd.h>
#include <iostream>
using namespace std;
const int times = 1000;
const int N = 100000;
void run() {
for (int j = 0; j < N; j++) {
}
}
int main() {
clock_t main_start = clock();
for (int i = 0; i < times; i++) {
clock_t start = clock();
run();
cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl;
//usleep(1000);
}
cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl;
}
다음은 예제 코드입니다. 타이밍 루프의 처음 26 회 반복에서 run
함수 비용은 약 0.4ms이지만 비용은 0.2ms로 감소합니다.
이 때 usleep
주석 처리되어, 지연 루프는 절대 과속하지, 모든 실행 0.4 밀리합니다. 왜?
코드는 g++ -O0
(최적화 없음)으로 컴파일 되므로 지연 루프가 최적화되지 않습니다. Intel (R) Core (TM) i3-3220 CPU @ 3.30GHz, 3.13.0-32 일반 Ubuntu 14.04.1 LTS (Trusty Tahr)에서 실행됩니다.
26 회 반복 후 Linux는 프로세스가 풀 타임 슬라이스를 연속으로 두 번 사용하기 때문에 CPU를 최대 클록 속도까지 올립니다 .
벽시계 시간 대신 성능 카운터로 확인하면 지연 루프 당 코어 클럭주기가 일정하게 유지되어 DVFS (모든 최신 CPU가 더 많은 에너지로 실행하는 데 사용 하는)의 효과 일뿐임을 확인할 수 있습니다. 대부분의 경우 효율적인 주파수 및 전압).
새로운 전원 관리 모드 (하드웨어가 클럭 속도를 완전히 제어하는 경우)에 대한 커널 지원을 사용하여 Skylake 에서 테스트 한 경우 램프 업이 훨씬 더 빠르게 발생합니다.
Turbo 를 사용하는 Intel CPU 에서 잠시 실행 상태로두면 열 제한에 따라 클럭 속도가 최대 지속 주파수로 다시 낮아지면 반복 당 시간이 다시 약간 증가하는 것을 볼 수 있습니다.
a를 도입usleep
하면 프로세스가 최소 주파수에서도 100 % 부하를 생성하지 않기 때문에 Linux의 CPU 주파수 거버너 가 클럭 속도를 높이는 것을 방지 합니다. (즉, 커널의 휴리스틱은 CPU가 실행중인 워크로드에 대해 충분히 빠르게 실행되고 있다고 결정합니다.)
다른 이론에 대한 의견 :
re : 잠재적 인 컨텍스트 전환 usleep
이 캐시를 오염시킬 수 있다는 David의 이론 : 일반적으로 나쁜 생각은 아니지만이 코드를 설명하는 데 도움이되지 않습니다.
캐시 / TLB 오염은이 실험에서 전혀 중요하지 않습니다 . 기본적으로 스택의 끝을 제외하고 메모리를 건 드리는 타이밍 창 안에는 아무것도 없습니다. 대부분의 시간은 int
스택 메모리 중 하나 에 만 닿는 작은 루프 (명령 캐시 1 줄)에서 소비됩니다 . 그 동안의 잠재적 인 캐시 오염 usleep
은이 코드에 대한 시간의 아주 작은 부분입니다 (실제 코드는 다를 것입니다)!
x86에 대한 자세한 내용 :
clock()
자체 호출 은 캐시 미스 일 수 있지만 코드 페치 캐시 미스는 측정 대상의 일부가 아닌 시작 시간 측정을 지연시킵니다. 두 번째 호출 clock()
은 캐시에서 여전히 뜨겁기 때문에 거의 지연되지 않습니다.
run
함수는 다른 캐시 라인 일 수있다 main
(사람 GCC 마크 main
가 덜 최적화 및 기타 냉 기능 / 데이터 도착 배치되도록 "콜드"등). 하나 또는 두 개의 명령 캐시 미스를 예상 할 수 있습니다 . 하지만 여전히 동일한 4k 페이지에 main
있을 수 있으므로 프로그램의 시간 제한 영역에 들어가기 전에 잠재적 인 TLB 미스를 유발 했을 것입니다.
gcc -O0은 OP의 코드를 다음과 같이 컴파일합니다 (Godbolt Compiler explorer) : 루프 카운터를 스택의 메모리에 유지합니다.
빈 루프는 루프 카운터를 스택 메모리에 유지하므로 일반적인 Intel x86 CPU 에서 루프는 OP의 IvyBridge CPU에서 ~ 6 사이클 당 한 번의 반복으로 실행됩니다. add
이는 메모리 대상 (읽기 -수정-쓰기). 100k iterations * 6 cycles/iteration
최대 2 개의 캐시 미스의 기여를 지배하는 600k주기입니다 (코드 가져 오기 미스에 대해 각각 ~ 200 주기로 해결 될 때까지 추가 명령이 발행되지 못함).
비 순차적 실행 및 저장 전달은 대부분 ( call
명령의 일부로) 스택에 액세스 할 때 잠재적 인 캐시 미스를 숨겨야합니다 .
루프 카운터가 레지스터에 보관되어 있어도 100k 사이클은 많습니다.
를 호출 usleep
하면 컨텍스트 전환이 발생할 수도 있고 그렇지 않을 수도 있습니다. 그렇다면 그렇지 않은 경우보다 더 오래 걸립니다.
'Development Tip' 카테고리의 다른 글
사용하지 않을 때 SQL Azure DB 중지 (0) | 2020.10.29 |
---|---|
angular2를 사용하여 서비스에서 구성 요소의 변수 변경 사항 업데이트 (0) | 2020.10.29 |
JavaScript에서 잠금을 구현하는 방법 (0) | 2020.10.29 |
Visual Studio 2010 용 서비스 팩 1이 설치되어 있는지 어떻게 알 수 있습니까? (0) | 2020.10.29 |
Typescript 기본 유형 : "숫자"유형과 "숫자"유형간에 차이가 있습니까 (TSC는 대소 문자를 구분하지 않음)? (0) | 2020.10.29 |