Эффективность опционации GCC на битовых операциях
Вот два способа установить отдельный бит в C на x86-64:
inline void SetBitC(long *array, int bit) {
//Pure C version
*array |= 1<<bit;
}
inline void SetBitASM(long *array, int bit) {
// Using inline x86 assembly
asm("bts %1,%0" : "+r" (*array) : "g" (bit));
}
Используя GCC 4.3 с параметрами -O3 -march=core2
, версия C занимает на 90% больше времени при использовании с константой bit
. (Обе версии составляют один и тот же код сборки, за исключением того, что версия C использует инструкцию or [1<<num],%rax
вместо инструкции bts [num],%rax
)
При использовании с переменной bit
версия C работает лучше, но все еще значительно медленнее, чем встроенная сборка.
Сброс, переключение и проверка битов имеют схожие результаты.
Почему GCC оптимизируется настолько плохо для такой общей операции? Я что-то делаю с версией C?
Изменить: Извините за долгое ожидание, вот код, который я использовал для сравнения. Это фактически началось как простая проблема программирования...
int main() {
// Get the sum of all integers from 1 to 2^28 with bit 11 always set
unsigned long i,j,c=0;
for (i=1; i<(1<<28); i++) {
j = i;
SetBit(&j, 10);
c += j;
}
printf("Result: %lu\n", c);
return 0;
}
gcc -O3 -march=core2 -pg test.c
./a.out
gprof
with ASM: 101.12 0.08 0.08 main
with C: 101.12 0.16 0.16 main
time ./a.out
также дает аналогичные результаты.
Ответы
Ответ 1
Почему GCC оптимизируется настолько плохо для такой общей операции?
Прелюдия: с конца 1980-х годов основное внимание на оптимизации компилятора отошло от микрообъектов, которые фокусируются на отдельных операциях, и на макропринципы, которые фокусируются на приложениях чья скорость людей волнует. В наши дни большинство авторов компилятора сосредоточены на макрографических марках, и разработка хороших эталонных наборов - это то, что воспринимается всерьез.
Ответ. Никто из gcc не использует тест, где разница между or
и bts
имеет значение для времени выполнения реальной программы. Если вы можете создать такую программу, вы можете привлечь внимание людей к gcc-land.
Я что-то делаю с версией C?
Нет, это совершенно хороший стандарт C. На самом деле очень читабельна и идиоматична.
Ответ 2
Можете ли вы опубликовать код, который вы используете, чтобы сделать выбор времени? Эта операция может быть сложной для времени точно.
Теоретически две кодовые последовательности должны быть одинаково быстрыми, поэтому наиболее вероятное объяснение (на мой взгляд) состоит в том, что что-то заставляет ваш код времени давать фиктивные результаты.
Ответ 3
Для такого кода:
#include <stdio.h>
#include <time.h>
int main() {
volatile long long i = 0;
time_t start = time (NULL);
for (long long n = 0; n < (1LL << 32); n++) {
i |= 1 << 10;
}
time_t end = time (NULL);
printf("C took %ds\n", (int)(end - start));
start = time (NULL);
for (long long n = 0; n < (1LL << 32); n++) {
__asm__ ("bts %[bit], %[i]"
: [i] "=r"(i)
: "[i]"(i), [bit] "i" (10));
}
end = time (NULL);
printf("ASM took %ds\n", (int)(end - start));
}
результат:
C took 12s
ASM took 10s
Мой флаг был (-std=gnu99 -O2 -march=core2
). Без изменчивости петля была оптимизирована. gcc 4.4.2.
Отличие не было:
__asm__ ("bts %[bit], %[i]"
: [i] "+m"(i)
: [bit] "r" (10));
Так что, наверное, ответ был: никто не заботится. В microbenchmark единственное различие заключается в том, что между этими двумя методами, но в реальной жизни я считаю, что такой код не требует большого количества CPU.
Дополнительно для такого кода:
#include <stdio.h>
#include <time.h>
int main() {
volatile long long i = 0;
time_t start = time (NULL);
for (long long n = 0; n < (1L << 32); n++) {
i |= 1 << (n % 32);
}
time_t end = time (NULL);
printf("C took %ds\n", (int)(end - start));
start = time (NULL);
for (long long n = 0; n < (1L << 32); n++) {
__asm__ ("bts %[bit], %[i]"
: [i] "+m"(i)
: [bit] "r" (n % 32));
}
end = time (NULL);
printf("ASM took %ds\n", (int)(end - start));
}
В результате получилось:
C took 9s
ASM took 10s
Оба результата были "стабильными". Тестирование CPU 'Intel (R) Core (TM) 2 Duo CPU T9600 @2.80GHz'.
Ответ 4
Это очень распространенная операция для встроенных систем, которые обычно ограничены ресурсами. 10 Циклы против 5 циклов - это отвратительное наказание за такие системы. Существует много случаев, когда вы хотите получить доступ к портам ввода-вывода или использовать 16 или 32-битные регистры в качестве булевых битовых флагов для сохранения памяти.
Дело в том, что if(bit_flags& 1<<12)
гораздо читабельнее [и переносится при реализации с помощью библиотеки], чем эквивалент сборки. Аналогично для IO_PINS|= 1<<5;
Это, к сожалению, много раз медленнее, поэтому неловкие макросы asm живут.
Во многих отношениях цели встроенных и пользовательских приложений противоположны. Ответственность внешних коммуникаций (для пользовательского интерфейса или машинного интерфейса) имеет второстепенное значение, в то время как обеспечение цикла управления (эквивалент микрочипа) завершается за минимальное время, является абсолютно критическим и может создавать или прерывать выбранный процессор или управление стратегия.
Очевидно, что, если вы можете позволить себе поддерживать процессор с несколькими ГГц и все связанные с ним периферийные устройства, чипсеты и т.д., чтобы поддерживать это, на самом деле не нужно беспокоиться о оптимизации на низком уровне. Более медленный микроконтроллер 1000 раз в системе управления в реальном времени означает, что сохранение тактовых циклов в 1000 раз более важно.
Ответ 5
Я думаю, вы много задаете свой оптимизатор.
Возможно, вы сможете немного помочь, сделав "register long z = 1L < бит;", то или -ий, что с вашим массивом.
Однако, я полагаю, что на 90% больше времени вы подразумеваете, что версия C занимает 10 циклов, а версия asm занимает 5 циклов, верно? Как сравнение производительности при -O2 или -O1?