1.
자본시장IT에 몸 담고 있으면 이제 latency는 아주 익숙한 단어입니다. Low Latency에 대한 관심도 높습니다. 때문에 DMA를 이용하고자 하고 부산IDC에 입주하려고 합니다. 그런데 여전히 latency를 상수이자 숫자로 보는 듯 합니다.
DMA매매를 하는 트레이더중 한 곳이 아닌 여러곳에서 매매를 합니다. 같은 서버, 같은 전략이므로 같은 체결율을 원합니다. 그렇지만 현실은 다릅니다.?예를 들면 이렇습니다.
“환경이 다 같은데 왜 느릴까?”
이렇게 반문을 해볼 수 있습니다.
“환경이 다 같으면 똑같을까?”
똑같을 수 없습니다. 100% 동일하다고 하여도 다릅니다. 때문에 Latency를 분포로 이야기합니다. Jitter때문입니다. 아주 간단한 실험을 해보았습니다. CPU와 Timer Latency의 관계를 다룬 글을 소스를 만들어 실행하였습니다.
How Accurate is Your Time? A Comparison of Timer Performance and Jitter on Intel vs Power
개발은 개발파트너가 담당하였습니다. Latency를 측정할 때 사용할 때 로그파일에 시간을 남겨서 비교합니다. 이 때 일반적으로 사용하는 함수가 gettimeofday()입니다.
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 |
#include <stdio.h> #include <time.h> #include <sys/time.h> #include <stdint.h> #define NANO_SECONDS_IN_SEC 1000000000L static inline struct timespec *TimeSpecDiff(struct timespec *ts1, struct timespec *ts2) { static struct timespec ts; ts.tv_sec = ts1->tv_sec - ts2->tv_sec; ts.tv_nsec = ts1->tv_nsec - ts2->tv_nsec; if (ts.tv_nsec < 0) { ts.tv_sec--; ts.tv_nsec += NANO_SECONDS_IN_SEC; } return &ts; } #define NUM_CALLS 100 #define NUM_RUNS 10000 int main(int argc, char* argv[]) { uint64_t accumulate = 0; double max = 0, min = 9999999999999L; unsigned x, y; struct timespec start, stop; struct timeval val1; for (x = 0; x < NUM_RUNS; x++) { clock_gettime(CLOCK_MONOTONIC, &start); for (y = 0; y < NUM_CALLS; y++) gettimeofday(&val1, NULL); clock_gettime(CLOCK_MONOTONIC, &stop); struct timespec* diff = TimeSpecDiff(&stop, &start); uint64_t diff_ns = diff->tv_sec * NANO_SECONDS_IN_SEC + diff->tv_nsec; double avg = (double)diff_ns / NUM_CALLS; printf("%10.2f\n", avg); if (avg > max) max = avg; if (avg < min) min = avg; accumulate += diff_ns; } printf("Max %10.2f\n", max); printf("Min %10.2f\n", min); printf("Avg %10.2f\n", (double)accumulate / (NUM_CALLS*NUM_RUNS)); |
이상을 컴파일하여 개발장비에서 실행을 하였습니다.
1 2 3 4 5 |
$ gcc -o Gettimeofday.x Gettimeofday.c -lrt $./Gettimeofday.x Max 1034.45 Min 24.55 Avg 31.11 |
최대와 최소값이 40배정도 차이가 납니다. 99%와 99.9%의 값을 구해보시길 바랍니다. 같은 서버에서 시간을 로글에 남기기 위해 gettimeofday()를 호출할 때마다 서로 다른 지연이 발생합니다. 같은 장비에서도 다른 값입니다. 하물며 100% 사양이 동일하고 프로그램이 같다고 하여 같은 값이 나올 수 없습니다.
2.
또다른 실험을 하였습니다. Linux의 Timer중 어떤 것이 가장 적은 비용=latency인지를 측정하였습니다. 이 또한 2010년에 쓰여진 블로그의 글을 참고로 하였습니다. Read Time Stamp Counter (RDTSC) 와 High Precision Event Timers (HPET)을 비교한 시험입니다.
High Performance Time Measurement in Linux
비교 프로그램의 소스는 아래와 같습니다.
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
#include <stdio.h> #include <stdint.h> /* for uint64_t */ #include <time.h> /* for struct timespec */ /* assembly code to read the TSC */ static inline uint64_t RDTSC() { unsigned int hi, lo; __asm__ volatile("rdtsc" : "=a" (lo), "=d" (hi)); return ((uint64_t)hi << 32) | lo; } const int NANO_SECONDS_IN_SEC = 1000000000; /* returns a static buffer of struct timespec with the time difference of ts1 and ts2 ts1 is assumed to be greater than ts2 */ struct timespec *TimeSpecDiff(struct timespec *ts1, struct timespec *ts2) { static struct timespec ts; ts.tv_sec = ts1->tv_sec - ts2->tv_sec; ts.tv_nsec = ts1->tv_nsec - ts2->tv_nsec; if (ts.tv_nsec < 0) { ts.tv_sec--; ts.tv_nsec += NANO_SECONDS_IN_SEC; } return &ts; } double g_TicksPerNanoSec; static void CalibrateTicks() { struct timespec begints, endts; uint64_t begin = 0, end = 0; clock_gettime(CLOCK_MONOTONIC, &begints); begin = RDTSC(); uint64_t i; for (i = 0; i < 1000000; i++); /* must be CPU intensive */ end = RDTSC(); clock_gettime(CLOCK_MONOTONIC, &endts); struct timespec *tmpts = TimeSpecDiff(&endts, &begints); uint64_t nsecElapsed = tmpts->tv_sec * 1000000000LL + tmpts->tv_nsec; g_TicksPerNanoSec = (double)(end - begin)/(double)nsecElapsed; } /* Call once before using RDTSC, has side effect of binding process to CPU1 */ void InitRdtsc() { unsigned long cpuMask; cpuMask = 2; // bind to cpu 1 sched_setaffinity(0, sizeof(cpuMask), &cpuMask); CalibrateTicks(); } void GetTimeSpec(struct timespec *ts, uint64_t nsecs) { ts->tv_sec = nsecs / NANO_SECONDS_IN_SEC; ts->tv_nsec = nsecs % NANO_SECONDS_IN_SEC; } /* ts will be filled with time converted from TSC reading */ void GetRdtscTime(struct timespec *ts) { GetTimeSpec(ts, RDTSC() / g_TicksPerNanoSec); } void TestHpet(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); } #define NUM_RUNS 100000000 int main(int argc, char* argv[]) { int i = 0; struct timespec start, stop; clock_gettime(CLOCK_MONOTONIC, &start); for (i = 0; i < NUM_RUNS; i++) TestHpet(); clock_gettime(CLOCK_MONOTONIC, &stop); struct timespec* diff = TimeSpecDiff(&stop, &start); printf("HPET : %ld.%06ld.%03ld\n", diff->tv_sec, diff->tv_nsec/1000, diff->tv_nsec-(diff->tv_nsec/1000*1000)); struct timespec rdtsc; InitRdtsc(); clock_gettime(CLOCK_MONOTONIC, &start); for (i = 0; i < NUM_RUNS; i++) GetRdtscTime(&rdtsc); clock_gettime(CLOCK_MONOTONIC, &stop); diff = TimeSpecDiff(&stop, &start); printf("RDTSC: %ld.%06ld.%03ld\n", diff->tv_sec, diff->tv_nsec/1000, diff->tv_nsec-(diff->tv_nsec/1000*1000)); |
천만번을 실행하였습니다. 그런데 재미난 결과를 보여줍니다. 먼저 특별한 옵션없이 컴파일을 한 후 실행하였습니다.
1 2 3 4 |
$gcc -o HpetVsRdtsc.x HpetVsRdtsc.c -lrt $ ./HpetVsRdtsc.x HPET : 2.222960.085 RDTSC: 3.418683.984 |
HPET가 더 좋습니다. 그런데 컴파일할 때 최적화옵션을 주었습니다. 결과가 반대로 나왔습니다. RDTSC가 3배정도 좋습니다.
1 2 3 4 |
$gcc -O2 -o HpetVsRdtsc.x HpetVsRdtsc.c -lrt $ ./HpetVsRdtsc.x HPET : 2.222230.114 RDTSC: 0.761423.661 |
앞서 소개한 블로그의 결과는 14배 정도 RDTSC가 좋다고 합니다. 어떻게 해야 할까요?
3.
Latency에 대한 관심이 이제는 Jitter로 옮겨져야 할 때입니다. 하드웨어와 네트워크 및 어플리케이션 을 Low Latency환경에 적합한 방향으로 구축하였습니다. 그렇지만 OS를 통하여 움직이기 때문에 Jitter는 어쩔 수 없습니다. 다만 편차를 줄이는 노력이 필요합니다. 가장 어렵고 힘든 과정인 튜닝입니다. 모범사례를 조사할 필요도 있지만 끊임없이 시험을 해보고 측정을 해야 합니다. 긴 과정입니다. Jitter때문에 FPGA와 같은 하드웨어방식으로 선호하는 경우도 있네요.