Итак, обработка независимых данных выполняется намного быстрее, но насколько быстро она выполняется? Увы, если от относительных величин перейти к абсолютным цифрам,– весь восторг мгновенно улетучится и наступит глубокое уныние.
Наивысшая пропускная способность, достигаемая при линейном чтении независимых данных, составляет не более чем 40%-50% от завяленной пропускной способности данного типа памяти. И это притом, что подсистема памяти для линейного доступа как раз и оптимизирована, и хаотичное чтение ячеек происходит, по меньшей мере, на порядок медленнее. А что может быть быстрее линейного доступа? (Аналогичный вопрос: что может быть короче, чем путь по прямой). Вот с поиска ответов на такие вопросы и начинается проникновение в истинную сущность предмета обсуждения.
В обработке независимых данных есть одна тонкость. Попытка одновременного чтения двух смежных ячеек в подавляющем большинстве случаев инициирует один, а не два запроса к подсистеме памяти. Это не покажется удивительным, если вспомнить, что минимальной порцией обмена с памятью является не отнюдь не байт, а целый пакет, длина которого в зависимости от типа процессора варьируется от 32- до 128 байт.
Таким образом, линейное чтение независимых данных еще не обеспечивает их параллельной обработки (обстоятельство, о котором популярные руководства по оптимизации склонны умалчивать). Вернемся к нашей программе (см. листинг [Memory/dependence.c]). Вот процессору потребовалось узнать содержимое ячейки *(int *) ((int) p1 + a). Он формирует запрос и направляет его чипсету, а сам тем временем приступает к обработке следующей команды – x += *(int *)((int)p1 + a + 4). "Ага", – думает процессор, – зависимости по данным нет и это хорошо! Но, с другой стороны… эта ячейка и без того возвратится с предыдущим запрошенным блоком, и посылать еще один запрос нет необходимости (чипсет, сколько его ни подгоняй, он быстрее работать не будет). Что ж, придется отложить выполнение данной команды до лучших времен.
Так, что там у нас дальше? Следующая команда – x += *(int *)((int)p1 + a + 8)
тоже отправляется на "консервацию", поскольку пытается прочесть ячейку из уже запрошенного блока. В общем, до тех пор, пока чипсет не обработает вверенный ему запрос, процессор (при линейном чтении данных!) ничего не делает, а только "складирует". Затем, по мере готовности операндов, команды "размораживаются" и процессор завершает их выполнение.
…наконец процессору встречается команда, обращающаяся к ячейке, следующей за концом последнего запрошенного блока. "Эх", – вздыхает процессор, – "если бы она мне встретилась раньше, – я бы смог отправить чипсету сразу два запроса!".
Более эффективный алгоритм обработки данных выглядит так: в первом проходе цикла память читается с шагом 32 байта (или 64/128 байт, если программа оптимизируется исключительно под Athlon/P-4), что заставляет процессор генерировать запросы чипсету при каждом обращении к памяти. В результате, на шине постоянно присутствуют несколько перекрывающихся запросов/ответов, обрабатывающихся параллельно (ну, почти параллельно). Во втором проходе цикла считываются все остальные ячейки, адреса которых не кратны 32 байтам. Поскольку, на момент завершения первого прохода они уже находятся в кэше, обращение к ним не вызовет больших задержек (см. рис 39).

