Нет, я могу, конечно, предположить, что ифы могут помешать конвейеризации. Я в этом не разбираюсь, но допустить могу. Однако: заведомо ясно, что любая процедурка, написанная с использованием алгоритмически напрашивающихся ифов -- будет заведомо более читабельна, чем любая другая, из которой те ифы исключительно из чувства пижонства исключены.
Она будет более читабельна (для неискушенного читателя) вне зависимости от того, исключены ли ифы из чувства пижонства или по другим соображениям. Более того, любые действия, выполненные исключительно из чувства пижонства, никаких хороших чувств не вызывают.
Читабельность программы - это действительно важно, однако во многих приложениях быстродействие является критическим фактором, и ради него приходится иногда читабельностью пренебречь. Человек, желающий в будущем программировать профессионально, должен это знать и уметь.
Дело действительно в конвейеризации. На современных процессорах условные переходы в программе действительно могут существенно замедлить быстродействие и даже свести на нет всякие алгоритмические ухищрения, в которых авторы пытаются "экономить операции". В процедурах с небольшим числом вычислений ифы могут съедать основное время работы.
Чтобы не быть голословным, я провел несколько простых экспериментов. Было создано два массива из ста миллионов чисел, заполненные случайно нулями и единицами. Основная процедура подсчитывала количество позиций, в которых в первом массиве стояла 1, а во втором - 0.
Первая реализация, самая примитивная:
int sum = 0;
for(int i = 0; i < N; ++i, ++p1, ++p2)
{
if( p1[0] != 0 && p2[0] == 0 )
++sum;
}
Не считая затрат на организацию цикла (включая проверку условия выхода) и инкремент sum, которые будут одинаковы во всех реализациях, мы имеем в теле цикла два if'а и две проверки на равенство и неравенство нулю. Один if написан явно, второй же возникает неявно в соответствии с правилами Си по вычислению логических операторов: после вычисления первого условия делается проверка и в случае, когда оно неверно, второе не вычисляется.
Данная реализация занимала примерно
500 миллисекунд времени.
(Замечу, что также ради небольшой потенциальной экономии я инкрементирую указатели на элементы массивов, вместо того, чтобы обращаться к ним по произвольному индексу p1[i] и p2[i]).
Вторая реализация (промежуточная): воспользуемся арифметикой и избавимся от неявного ифа. Разумеется, этот подход работает только потому, что у нас по условию значения могут быть только 0 или 1:
int sum = 0;
for(int i = 0; i < N; ++i, ++p1, ++p2)
{
if( p1[0] * (1 - p2[0]) != 0 )
++sum;
}
Мы избавились от промежуточного условного перехода, но добавили операцию вычитания и умножения. Результат -
360 миллисекунд. Относительное ускорение уже заметное. Но не будем останавливаться на достигнутом.
Разумеется, совершенно понятно, что if тут вообще не по делу, так как проверяемое выражение принимает значения только 0 или 1. Отсюда третья реализация:
int sum = 0;
for(int i = 0; i < N; ++i, ++p1, ++p2)
{
sum += p1[0] * (1 - p2[0]);
}
Время выполнения такой версии -
80 миллисекунд. Только за счет того, что убран условный переход. Ускорение относительно первоначальной реализации - более чем в 5 раз.
Но и это еще не все. Можно применить еще один важный метод ускорения - массовые операции. Фактически ведь мы работаем с битами, поэтому можем упаковывать их в группы и применять побитовые операции, которые выполняются еще быстрее, чем арифметические. Итак, заводим массивы в 8 раз меньше, чем исходные, и в каждый байт нового массива будем класть по 8 исходных значений. Сохранение значения x, имеющего исходный индекс i, в элемент нового массива arr, производится операцией:
Код:
arr[i/8] |= x<<(i%8)
Еще нам потребуется определять количество единичных бит в байте. Это удобнее всего делать по таблице: заранее заготовить соответствующий массив значений длины 256. Обозначим его Table. Реализация при этом выглядит так (n - это одна восьмая от количества чисел в исходном массиве):
int sum = 0;
for(int i = 0; i < n; ++i, ++p1, ++p2)
{
sum += Table[p1[0] & (~p2[0])];
}
Заметим, что этот подход требует в 8 раз меньше памяти под хранение данных. Время работы -
16 миллисекунд. Ускорение по сравнению с исходным примитивным вариантом - более чем в 30 раз.