Разница в производительности функции при передаче параметра в виде постоянной или переменной времени компиляции

В коде ядра 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 используется в. Могут быть более конкретные случаи, когда такая оптимизация будет невозможна.

Другие вопросы по тегам