Почему __builtin_popcount медленнее, чем моя собственная функция подсчета битов? - PullRequest
0 голосов
/ 04 сентября 2018

Я наткнулся на __builtin_popcount для gcc после того, как написал свои собственные процедуры подсчета битов. Но когда я переключился на __builtin_popcount, мое программное обеспечение работало медленнее. Я на Unbutu на процессоре Intel Core i3-4130T @ 2,90 ГГц. Я построил тест производительности, чтобы увидеть, что дает. Это выглядит так:

#include <iostream>
#include <sys/time.h>
#include <stdint.h>

using namespace std;

const int bitCount[256] = {
    0,1,1,2,1,2,2,3,  1,2,2,3,2,3,3,4,  1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,
    1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,  2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,
    1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,  2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,
    2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,  3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,
    1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,  2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,
    2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,  3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,
    2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,  3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,
    3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,  4,5,5,6,5,6,6,7,  5,6,6,7,6,7,7,8
};

const uint32_t m32_0001 = 0x000000ffu;
const uint32_t m32_0010 = 0x0000ff00u;
const uint32_t m32_0100 = 0x00ff0000u;
const uint32_t m32_1000 = 0xff000000u;

inline int countBits(uint32_t bitField)
{
    return
        bitCount[(bitField & m32_0001)      ] +
        bitCount[(bitField & m32_0010) >>  8] +
        bitCount[(bitField & m32_0100) >> 16] +
        bitCount[(bitField & m32_1000) >> 24];
}

inline long long currentTime() {
    struct timeval ct;
    gettimeofday(&ct, NULL);
    return ct.tv_sec * 1000000LL + ct.tv_usec;
}

int main() {
    long long start, delta, sum;

    start = currentTime();
    sum = 0;
    for(unsigned i = 0; i < 100000000; ++i)
        sum += countBits(i);
    delta = currentTime() - start;
    cout << "countBits         : sum=" << sum << ": time (usec)=" << delta << endl;

    start = currentTime();
    sum = 0;
    for(unsigned i = 0; i < 100000000; ++i)
        sum += __builtin_popcount(i);
    delta = currentTime() - start;
    cout << "__builtin_popcount: sum=" << sum << ": time (usec)=" << delta << endl;

    start = currentTime();
    sum = 0;
    for(unsigned i = 0; i < 100000000; ++i) {
        int count;
        asm("popcnt %1,%0" : "=r"(count) : "rm"(i) : "cc");
        sum += count;
    }
    delta = currentTime() - start;
    cout << "assembler         : sum=" << sum << ": time (usec)=" << delta << endl;

    return 0;
}

Сначала я запустил это со старым компилятором:

> g++ --version | head -1
g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=148506
__builtin_popcount: sum=1314447104: time (usec)=345122
assembler         : sum=1314447104: time (usec)=138036

Как видите, countBits на основе таблицы почти так же быстр, как ассемблер, и намного быстрее, чем __builtin_popcount. Затем я попробовал новый компилятор на другом типе машины (тот же процессор - и я думаю, что материнская плата тоже):

> g++ --version | head -1
g++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=164247
__builtin_popcount: sum=1314447104: time (usec)=345167
assembler         : sum=1314447104: time (usec)=138028

Любопытно, что старый компилятор оптимизировал мою функцию countBits лучше, чем новый компилятор, но он все равно выгодно отличается от ассемблера. Ясно, что поскольку строка ассемблера компилируется и запускается, мой процессор поддерживает popcount, но почему тогда __builtin_popcount более чем в два раза медленнее? И как моя собственная рутина может конкурировать с силиконовым поп-счетом? У меня такой же опыт с другими подпрограммами для поиска первого установленного бита и т. Д. Все мои подпрограммы значительно быстрее, чем встроенные эквиваленты GNU.

(Кстати, я понятия не имею, как писать на ассемблере. Я только что нашел эту строку на какой-то веб-странице, и она чудесным образом, похоже, сработала.)

Ответы [ 2 ]

0 голосов
/ 04 сентября 2018

Я подумал, что было бы полезно поделиться новыми результатами производительности после добавления -march = native к строке компиляции (как предложено Mat и Alan Birtles), которое позволяет использовать машинную инструкцию popcount. Результаты различаются в зависимости от версии компилятора. Вот старый компилятор:

> g++ --version | head -1
g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -march=native -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=163947
__builtin_popcount: sum=1314447104: time (usec)=138046
assembler         : sum=1314447104: time (usec)=138036

А вот и новый компилятор:

> g++ --version | head -1
g++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -march=native -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=163133
__builtin_popcount: sum=1314447104: time (usec)=73987
assembler         : sum=1314447104: time (usec)=138036

Замечания:

  1. Добавление -march = native в командную строку более старого g ++ компилятор улучшил производительность __builtin_popcount до равного что ассемблера, и замедлил мою процедуру подсчета битов примерно на 15%.

  2. Добавление -march = native в командную строку более новой версии g ++ компилятор заставил производительность __builtin_popcount превзойти что ассемблера. Я предполагаю, что это как-то связано с Переменная стека, которую я использовал с ассемблером, хотя я не уверен. Там не повлияло на мою производительность countBits (которая, как указано в моем вопрос, был уже медленнее с этим новым компилятором.)

0 голосов
/ 04 сентября 2018

Без указания соответствующего «-march» в командной строке gcc генерирует вызов функции __popcountdi2, а не инструкции popcnt. Смотри: https://godbolt.org/z/z1BihM

POPCNT поддерживается Intel со времен Nehalem и AMD со времен Барселоны согласно википедии: https://en.wikipedia.org/wiki/SSE4#POPCNT_and_LZCNT

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...