Разница в производительности функции при передаче параметра в виде постоянной или переменной времени компиляции
В коде ядра Linux есть макрос, используемый для проверки бита (версия Linux 2.6.2):
#define test_bit(nr, addr) \
(__builtin_constant_p((nr)) \
? constant_test_bit((nr), (addr)) \
: variable_test_bit((nr), (addr)))
где constant_test_bit
а также variable_test_bit
определяются как:
static inline int constant_test_bit(int nr, const volatile unsigned long *addr )
{
return ((1UL << (nr & 31)) & (addr[nr >> 5])) != 0;
}
static __inline__ int variable_test_bit(int nr, const volatile unsigned long *addr)
{
int oldbit;
__asm__ __volatile__(
"btl %2,%1\n\tsbbl %0,%0"
:"=r" (oldbit)
:"m" (ADDR),"Ir" (nr));
return oldbit;
}
Я это понимаю __builtin_constant_p
используется для определения, является ли переменная постоянной времени компиляции или неизвестной. Мой вопрос: есть ли разница в производительности между этими двумя функциями, когда аргумент является постоянной времени компиляции или нет? Зачем использовать версию C, когда она есть, и версию сборки, если это не так?
ОБНОВЛЕНИЕ: Следующая основная функция используется для проверки производительности:
константа, вызов константы_test_bit:
int main(void) {
unsigned long i, j = 21;
unsigned long cnt = 0;
srand(111)
//j = rand() % 31;
for (i = 1; i < (1 << 30); i++) {
j = (j + 1) % 28;
if (constant_test_bit(j, &i))
cnt++;
}
if (__builtin_constant_p(j))
printf("j is a compile time constant\n");
return 0;
}
Это правильно выводит предложение j является...
Для других ситуаций просто раскомментируйте строку, которая присваивает "случайное" число j
и измените имя функции соответственно. Когда эта строка не закомментирована, вывод будет пустым, и это ожидается.
я использую gcc test.c -O1
скомпилировать, и вот результат:
константа, константа_тест_бит:
$ time ./a.out
j is compile time constant
real 0m0.454s
user 0m0.450s
sys 0m0.000s
константа, variable_test_bit (опустить time ./a.out
, то же самое для следующего):
j is compile time constant
real 0m0.885s
user 0m0.883s
sys 0m0.000s
переменная, константа_тест_бит:
real 0m0.485s
user 0m0.477s
sys 0m0.007s
переменная, variable_test_bit:
real 0m3.471s
user 0m3.467s
sys 0m0.000s
У меня каждая версия запускается несколько раз, и приведенные выше результаты являются их типичными значениями. Кажется constant_test_bit
функция всегда быстрее, чем variable_test_bit
функция, независимо от того, является ли параметр постоянной времени компиляции или нет... Для двух последних результатов (когда j
не является постоянной) версия переменной даже значительно медленнее, чем постоянная. Я что-то здесь упускаю?
1 ответ
Используя godbolt, мы можем провести эксперимент с использованием constant_test_bit, следующие две тестовые функции скомпилированы gcc
с -O3
флаг:
// Non constant expression test case
int func1(unsigned long i, unsigned long j)
{
int x = constant_test_bit(j, &i) ;
return x ;
}
// constant expression test case
int func2(unsigned long i)
{
int x = constant_test_bit(21, &i) ;
return x ;
}
Мы видим, что оптимизатор может оптимизировать регистр констант для следующего:
shrq $21, %rax
andl $1, %eax
в то время как случай неконстантного выражения заканчивается следующим образом:
sarl $5, %eax
andl $31, %ecx
cltq
leaq -8(%rsp,%rax,8), %rax
movq (%rax), %rax
shrq %cl, %rax
andl $1, %eax
Таким образом, оптимизатор может создавать намного лучший код для случая с постоянным выражением, и мы видим, что случай с непостоянным значением для constant_test_bit
довольно плохо по сравнению с собранной вручную в variable_test_bit
и исполнитель должен верить случаю константного выражения для constant_test_bit
в итоге оказывается лучше чем:
btl %edi,8(%rsp)
sbbl %esi,%esi
для большинства случаев.
Относительно того, почему ваш тестовый пример, кажется, показывает другой вывод, что ваш тестовый пример имеет недостатки. Я не смог разобраться во всех вопросах. Но если мы посмотрим на этот случай с помощью constant_test_bit
с непостоянным выражением мы видим, что оптимизатор может переместить всю работу за пределы внешнего вида и уменьшить работу, связанную с constant_test_bit
внутри цикла, чтобы:
movq (%rax), %rdi
даже со взрослым gcc
версия, но этот случай может не относиться к случаям test_bit
используется в. Могут быть более конкретные случаи, когда такая оптимизация будет невозможна.