繁忙的循环减慢了延迟关键计算
Busy loop slows down latency-critical computation
我的代码执行以下操作:
- 做一些长时间的运行密集计算(下面称为无用)
- 做一个小的延迟关键任务
我发现使用长 运行 计算执行延迟关键任务所需的时间比没有长计算要长。
下面是一些重现此效果的独立 C++ 代码:
#include <stdio.h>
#include <stdint.h>
#define LEN 128
#define USELESS 1000000000
//#define USELESS 0
// Read timestamp counter
static inline long long get_cycles()
{
unsigned low, high;
unsigned long long val;
asm volatile ("rdtsc" : "=a" (low), "=d" (high));
val = high;
val = (val << 32) | low;
return val;
}
// Compute a simple hash
static inline uint32_t hash(uint32_t *arr, int n)
{
uint32_t ret = 0;
for(int i = 0; i < n; i++) {
ret = (ret + (324723947 + arr[i])) ^ 93485734985;
}
return ret;
}
int main()
{
uint32_t sum = 0; // For adding dependencies
uint32_t arr[LEN]; // We'll compute the hash of this array
for(int iter = 0; iter < 3; iter++) {
// Create a new array to hash for this iteration
for(int i = 0; i < LEN; i++) {
arr[i] = (iter + i);
}
// Do intense computation
for(int useless = 0; useless < USELESS; useless++) {
sum += (sum + useless) * (sum + useless);
}
// Do the latency-critical task
long long start_cycles = get_cycles() + (sum & 1);
sum += hash(arr, LEN);
long long end_cycles = get_cycles() + (sum & 1);
printf("Iteration %d cycles: %lld\n", iter, end_cycles - start_cycles);
}
}
当使用 -O3
编译且 USELESS
设置为 10 亿时,三个迭代分别花费了 588、4184 和 536 个周期。在 USELESS
设置为 0 的情况下进行编译时,迭代分别需要 394、358 和 362 个周期。
为什么会发生这种情况(尤其是 4184 个周期)?我怀疑由密集计算引起的缓存未命中或分支预测错误。然而,如果没有密集的计算,延迟关键任务的第零次迭代非常快,所以我不认为冷 cache/branch 预测器是原因。
将我的推测性评论移至答案:
有可能当你的忙循环是运行时,服务器上的其他任务正在将缓存的arr
数据从L1缓存中推出,所以[=中的第一个内存访问11=] 需要从较低级别的缓存中重新加载。如果没有计算循环,这不会发生。您可以尝试将 arr
初始化移动到计算循环之后,看看效果如何。
我的代码执行以下操作:
- 做一些长时间的运行密集计算(下面称为无用)
- 做一个小的延迟关键任务
我发现使用长 运行 计算执行延迟关键任务所需的时间比没有长计算要长。
下面是一些重现此效果的独立 C++ 代码:
#include <stdio.h>
#include <stdint.h>
#define LEN 128
#define USELESS 1000000000
//#define USELESS 0
// Read timestamp counter
static inline long long get_cycles()
{
unsigned low, high;
unsigned long long val;
asm volatile ("rdtsc" : "=a" (low), "=d" (high));
val = high;
val = (val << 32) | low;
return val;
}
// Compute a simple hash
static inline uint32_t hash(uint32_t *arr, int n)
{
uint32_t ret = 0;
for(int i = 0; i < n; i++) {
ret = (ret + (324723947 + arr[i])) ^ 93485734985;
}
return ret;
}
int main()
{
uint32_t sum = 0; // For adding dependencies
uint32_t arr[LEN]; // We'll compute the hash of this array
for(int iter = 0; iter < 3; iter++) {
// Create a new array to hash for this iteration
for(int i = 0; i < LEN; i++) {
arr[i] = (iter + i);
}
// Do intense computation
for(int useless = 0; useless < USELESS; useless++) {
sum += (sum + useless) * (sum + useless);
}
// Do the latency-critical task
long long start_cycles = get_cycles() + (sum & 1);
sum += hash(arr, LEN);
long long end_cycles = get_cycles() + (sum & 1);
printf("Iteration %d cycles: %lld\n", iter, end_cycles - start_cycles);
}
}
当使用 -O3
编译且 USELESS
设置为 10 亿时,三个迭代分别花费了 588、4184 和 536 个周期。在 USELESS
设置为 0 的情况下进行编译时,迭代分别需要 394、358 和 362 个周期。
为什么会发生这种情况(尤其是 4184 个周期)?我怀疑由密集计算引起的缓存未命中或分支预测错误。然而,如果没有密集的计算,延迟关键任务的第零次迭代非常快,所以我不认为冷 cache/branch 预测器是原因。
将我的推测性评论移至答案:
有可能当你的忙循环是运行时,服务器上的其他任务正在将缓存的arr
数据从L1缓存中推出,所以[=中的第一个内存访问11=] 需要从较低级别的缓存中重新加载。如果没有计算循环,这不会发生。您可以尝试将 arr
初始化移动到计算循环之后,看看效果如何。