Давайте изобретем систему баллов и представим, что выборка данных из кэша L1 стоит 4 балла, выборка из кэша L2 - 8 баллов, а непредсказуемая ветвь - 12 баллов.Обратите внимание, что эти точки выбраны для грубого представления «циклов для среднего, но неизвестного процессора 80x86».
Исходный код с одной таблицей ввода 1024 будет иметь общую стоимость 4 балла за итерацию (при условии, что это часто делаетсядостаточно, чтобы производительность имела значение, и, следовательно, при условии, что данные используются достаточно часто, чтобы быть в кеше L1).
С оператором switch компилятор собирается (надеюсь - серия, если ветки - это кошмар производительности)преобразовать его в таблицу переходов и сделать что-то вроде goto table[i];
, так что это, вероятно, считается как выборка из таблицы (4 балла), за которой следует одна непредсказуемая ветвь (12 баллов);или всего 16 точек за итерацию.
Обратите внимание, что для 64-битного кода таблица переходов, которую генерирует компилятор, будет 1024 записями, где каждая запись составляет 64 бита;и эта таблица будет в два раза больше таблицы для первого варианта (которая составляет 1024 записи, где каждая запись составляет 32 бита).Однако кэши данных L1 во многих ЦП имеют размер 64 КБ, поэтому таблица переходов 64 КБ означает все остальное, что входит в кэш данных L1 (исходные данные ANDed, результирующие данные «ответа», что-либо в стеке ЦП)заставляет (64 байта или 8 записей) части вашей таблицы переходов быть удаленными из кэша, чтобы освободить место.Это означает, что иногда вы будете платить за «L1 miss, L2 hit».Давайте предположим, что это происходит в 5% случаев, поэтому реальные затраты в конечном итоге составляют «(95 * (4 + 12) + 5 * (8 + 12)) / 100 = 16,2» балла за итерацию.
Учитывая, что вы ожидаете, что производительность будет выше для первого варианта («16,2 балла за итерацию» значительно больше, чем «4 балла за итерацию»), и что вы ожидаете, что размер исполняемого файла будет лучше для первого варианта (дажебез учета какого-либо кода для каждого case
из switch
таблица 32 КиБ - это половина размера таблицы 64 КиБ), и учитывая, что первый вариант имеет более простой (более обслуживаемый) код;Я не вижу ни одной причины, по которой вы захотите использовать второй вариант.
Чтобы оптимизировать этот код, я бы попробовал поработать над большими кусками.Для простого примера, можете ли вы сделать что-то вроде этого:
uint64_t mask[512] = { ....
uint64_t workingdata;
uint64_t temp;
for (i=0; i<512; i++)
{
workingdata = getnextdatachunk() << 32 | getnextdatachunk();
temp = workingdata & mask[i];
answer[i*2] = temp;
answer[i*2+1] = temp >> 32;
}
Если вы можете сделать что-то подобное, то это может (в лучшем случае) удвоить производительность;но если вы можете сделать «64 бита на итерацию для половины итераций», вы также сможете использовать встроенные функции SIMD для «128 бит на итерацию для четверти итераций» или «256 бит на итерацию для восьмой числаитераций ", и может быть в состоянии сделать это почти в 8 раз быстрее.
Конечно, шаг за этим заключается в том, чтобы буферизовать достаточное количество исходных данных, чтобы сделать использование нескольких потоков (нескольких процессоров) эффективным (например, чтобызатраты на синхронизацию могут быть эффективно амортизированы).При 4 параллельных процессорах, выполняющих по 256 бит на каждую итерацию, вы получите (теоретически лучший вариант) ускорение «в 32 раза быстрее, чем исходные 1024 итерации, 32 бит на итерацию с одной версией процессора».