Рассмотрим следующий заголовочный файл "tls.h":
#include <stdint.h>
// calling this function is expensive
uint64_t foo(uint64_t x);
extern __thread uint64_t cache;
static inline uint64_t
get(uint64_t x)
{
// if cache is not valid
if (cache == UINT64_MAX)
cache = foo(x);
return cache + x;
}
и исходный файл "tls.c":
#include "tls.h"
__thread uint64_t cache = {0};
uint64_t foo(uint64_t x)
{
// imagine some calculations are performed here
return 0;
}
Ниже приведен пример использования функции get()
в "main.c":
#include "tls.h"
uint64_t t = 0;
int main()
{
uint64_t x = 0;
for(uint64_t i = 0; i < 1024UL * 1024 * 1024; i++){
t += get(i);
x++;
}
}
Представленные файлы компилируются следующим образом:
gcc -c -O3 tls.c
gcc -c -O3 main.c
gcc -O3 main.o tls.o
Изучение производительности цикла в "main.c" показало, что оптимизация компилятора очень плохая,После разборки двоичного файла становится ясно, что доступ к tls осуществляется на каждой итерации.Время выполнения на моей машине составляет 1.7 с.
Однако, если я уберу проверку проверки кэша в методе get()
, чтобы он выглядел так:
static inline uint64_t
get(uint64_t x)
{
return cache + x;
}
, компилятор теперь можетсоздавать гораздо более быстрый код - он полностью удаляет цикл и генерирует только одну инструкцию «добавить».Время выполнения составляет ~ 0,02 с.
Почему компилятор не может оптимизировать первый случай?Переменная TLS не может быть изменена другими потоками, поэтому компилятор должен быть в состоянии оптимизировать это, верно?
Есть ли другой способ оптимизировать функцию get()
?