Буфера записи
Для предотвращения задержки, возникающей при промахах записи, современные процессоры активно используют различные приемы буферизации. Вместо того, чтобы немедленно отправлять записываемые данные по месту назначения, процессор временно помещает их в специальный буфер, откуда, по мере освобождения шины и/или кэш-контроллера они выгружаются в кэш первого (второго) уровня или в основную оперативную память.
Такая схема особенно полезна при значительном превышении количества операций чтения над числом операций записи. Если частота возникновения промахов записи не превышает скорости выгрузки буферов, штрафных задержек не возникает вовсе и эффективная скорость выполнения инструкции записи составляет 1 такт.
Часто приходится слышать: чем больше в процессоре буферов, тем выше предельная частота промахов записи, которую без ущерба для производительности он способен выдержать. На самом деле, это неверно. Максимальная частота промахов определяется не количеством буферов, а скоростью (и политикой) их выгрузки.
В частности, процессоры AMD K6 и Athlon всегда выгружают содержимое буферов записи в кэш первого уровня, благодаря чему скорость их опорожнения весьма велика (при благоприятном стечении обстоятельств каждый 32/64? байтовый буфер выгружается за один такт), а выгруженные данные вплоть до вытеснения их из L1-кэша доступны на чтение практически мгновенно!
Процессоры P6 и P-4, напротив, направляют содержимое буферов записи в кэш второго, а не первого уровня. (Конечно, если записываемые данные уже содержаться в L1-кэше они помещаются именно туда). Эффективность такой стратегии не бесспорна. С одной стороны: это приводит к тому, что процессор переносит значительно меньшую интенсивность промахов записи, а с другой: выгрузка записываемых данных в кэш второго уровня не загрязняет кэш первого уровня, в конечном итоге увеличивая его эффективную емкость.
Кстати, нетривиальным следствием такой политики выгрузки буферов, становится искажение политики кэш-записи. Несмотря на то, что кэш первого уровня формально построен по Write Back архитектуре, фактически он работает по схеме Write True, поскольку промах записи приводит к непосредственному обновлению кэш?памяти более высокой иерархии без загрузки модифицируемых данных в кэш.
Забавно, но Intel впервые "проговорилась" об этом факте лишь в документации по процессору Pentium-4, где в графе " политика записи кэша первого уровня" честно указано "Write True", а не "Write Back", как это утверждалось в документации по процессорам предыдущего поколения.
Важно понять, что увеличение количества буферов записи не компенсируют медлительность их выгрузки в память. Практически единственная польза емкого буфера в том, что он безболезненно переносит локальные
перегрузки, когда несколько промахов возникают одновременно (или идут с минимальным отрывом друг от друга), а затем вновь наступает "тишина".
Чтение после записи. Вопреки своему названию, каждый из буферов доступен не только на запись, но и на чтение. Причем, чтение данных из буфера записи осуществляется по крайней мере на один такт быстрее, нежели из кэш-памяти первого уровня. (Подробнее о том, как можно использовать это обстоятельство для оптимизации своих программ, рассказывается в "Оптимизация обращения к памяти и кэшу. Особенности буферизации записи").
Тем не менее, чтение содержимого буферов (особенно на процессорах семейства Pentium) следует осуществлять с большой осторожностью, т.к. только что записанные данные в любой момент могут перекочевать в кэш второго уровня, что резко увеличит время доступа к ним. Помимо того, что буфера самопроизвольно выгружаются во время простоя шины, они незамедлительно опоражниваются в следующих ситуациях:
· при выполнении инструкции с префиксом монопольного захвата шины (LOCK);
· при выполнении инструкции упорядоченного выполнения (например, CPUID);
· при выполнении инструкции выгрузки буферов SFNCE (Pentium III и выше);
· при выполнении инструкции выгрузки буферов MFENCE (Pentium-4)
· при возникновении исключения или вызове прерывания;
· при записи или чтении в/из порта ввода-вывода;
· при выполнении инструкции BINIT;
Упорядочивание записи. Помимо всего прочего, на буфера еще возложена и функция упорядочивания записи. Старшие представители семейства x86 разбивают машинные инструкции на микрооперации, выполняя их в наиболее предпочтительном с точки зрения RISC-ядра процессора, порядке. Но ведь нарушение очередности операций чтения/записи в память (кэш) может запросто нарушить нормальную работу программы!
Задумайтесь, что произойдет если в следующем блоке кода "a = *p; *p = b;" запись ячейки *p завершится раньше ее чтения? Спрашивайте, а почему такое вообще может случиться? Да мало ли почему! Допустим, блок чтения данных занят обработкой предыдущей инструкции и команда a = *p
вынуждена терпеливо дожидаться свой очереди, в то время как команда *p = b
захватывается бездельничающим в этот момент блоком записи.
Конечно, блок записи можно до завершения чтения и притормозить, но производительности такая мера не добавит – это уж точно. Выход?! Выход: временно сохранить записываемые данные не в кэше (основной памяти), а в некотором промежуточном буфере. Чтобы не захламлять архитектуру микроядра и не вводить еще один буфер, конструкторы решили для этих целей использовать все те же буфера записи. Политика выгрузки данных из буфера гарантирует, что данные, помещенные в буфер, покинут его "застенки" не раньше, чем завершаться все, предшествующие им инструкции. Таким образом, с буфера данные сходят уже упорядоченными и потому никаких конфликтов не возникает.
Причем, буферизация записи в определенной степени сокращает количество обращений к памяти, поскольку если одна и та же ячейка записывалась несколько раз в память (кэш) попадает лишь последний результат.
Реализация и характеристики буферов записи.
Младшие модели Pentium имели всего два буфера записи (write buffers) – по одному на каждую U- и V-трубу, но уже в Pentium MMX количество буферов возросло до четырех, а в Pentium II их и вовсе насчитывается двенадцать! Причем, начиная с Pentium II буфера записи переименованы в Складские Буфера (store buffers), однако, во избежании путаницы, здесь и далее мы будем пользоваться исключительно прежним термином. Все равно "store" и "write" в русском переводе – синонимы (во всяком случае, в рамках данного контекста)
Учитывая, что размер каждого из буферов составляет 32 байт, можно безболезненно сохранять до 384 байт (96 двойных слов) за раз, не беспокоясь за кэш-промахи. Но помните, что попытки записи в некэшируемую память при полностью заполненный буферах приводят к неустранимым кэш-промахам и вытекающих отсюда штрафным задержкам.
Поэтому, целесообразно чередовать операции записи с вычислениями – давая буферам время на выгрузку своего содержимого.