Tech/Linux

정확한 함수 실행 시간 측정: rdtsc, lfence 및 rdtscp 활용하기

아다지에토 2023. 3. 18. 17:12

우리는 함수의 실행시간을 정말로 정확하게 측정하고있을까?

 

cpu instruction을 이용하여 cycle을 측정한다면 가장 정확하게 시간을 측정할 수 있다.

하지만, 시간 측정을 위해 x86의 rdtsc, rdtscp를 사용하여도 부정확한 결과가 나올 수 있다. 이는 serializable하지 않은 코드 실행에 있다. 참고(link:https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf)

 

간단히 말하면 우리가 짠 코드가 컴파일 중 순서가 변경되어 serializable하지 않게 실행되며, 부정확한 함수 실행시간이 측정되는 것이다.

 

명령어들이 serializable하게 실행되지 않으면, 다음과 같은 문제가 발생할 수 있다.

  1. 명령어의 순서가 변경됨 컴파일러 또는 프로세서의 최적화로 인해 명령어의 순서가 변경되고 이로 인해 실행 순서와 실제 수행 순서 사이에 불일치가 발생하며, 예상치 못한 결과를 초래할 수 있다.
  2. 메모리의 일관성이 깨질 수 있음 멀티코어 또는 멀티프로세서 시스템에서 명령어들이 serializable하게 실행되지 않으면, 메모리의 일관성이 깨질 수 있고, 이는 여러 프로세서 간의 메모리 접근 경쟁으로 인해 데이터 무결성이 손상될 수 있다.
  3. 시간 측정의 정확성이 떨어질 수 있음 명령어들이 serializable하게 실행되지 않으면, 시간 측정의 정확성이 떨어질 수 있다. 명령어 파이프라인의 완료를 보장할 수 없기 때문에, 측정하려는 함수의 실행이 완전히 완료되기 전에 시간을 측정할 수도 있다.

 

이러한 문제를 방지하기 위해 시스템에서 필요한 경우 명령어들을 serializable하게 실행하도록 보장해야 한다. 이를 위해 메모리 배리어, 동기화 기법, 시리얼라이즈 명령어 (예: cpuid, lfence, mfence, sfence, rdtscp 등)를 사용할 수 있다. 이러한 방법들은 명령어 실행 순서를 제어하고 동시성 문제를 방지하는 데 도움이 된다.

 

Intel의 문서에는 cpuid를 사용하였지만, 이는 최근 lfence로 교체하여 사용되고 있다. 아래는 lfence, rdtsc, rdtscp를 이용하여 cpu cycle을 측정하는 코드이다.

 

#include <stdio.h>
#include <stdint.h>

// lfence를 이용하여 시리얼라이즈
static inline void lfence(void) {
    __asm__ __volatile__(
        "lfence" : : : "memory"
    );
}

// rdtsc를 이용한 현재 타임스탬프 측정 함수
static inline uint64_t rdtsc(void) {
    uint32_t low, high;
    __asm__ __volatile__(
        "rdtsc" : "=a" (low), "=d" (high)
    );
    return ((uint64_t)high << 32) | low;
}

// rdtscp를 이용한 현재 타임스탬프 측정 함수
static inline uint64_t rdtscp(void) {
    uint32_t low, high;
    uint32_t aux;
    __asm__ __volatile__(
        "rdtscp" : "=a" (low), "=d" (high), "=c" (aux)
    );
    return ((uint64_t)high << 32) | low;
}

void some_function() {
    // 이곳에 측정하고자 하는 코드 또는 함수를 작성하세요.
}

int main(void) {
    uint64_t start, end;

    // 시작에 rdtsc, lfence 사용; 끝에 lfence, rdtscp 사용
    start = rdtsc();
    lfence(); // 시리얼라이즈
    some_function();
    lfence(); // 시리얼라이즈
    end = rdtscp();
    printf("Elapsed time using rdtsc, lfence at start and lfence, rdtscp at end: %llu cycles\n", (unsigned long long)(end - start));

    return 0;
}

 

만약 실제 실행시간(예: sec.)를 구하고 싶다면, CPU cycle로 나눠주면 된다. 

예를들어 2.6 GHz를 사용한다면, (end-start) /  (2.6*1000*1000*1000)로 구할 수 있다.

 

 

끝.