Дядя Herb Sutter работает в MS, и иногда устраивает аццкие отжыги.
Я наконец-то на один такой попал, и очень радовался. На умных мужиков всегда радостно поглядеть и послушать.
Для отчета - кроме Херба, видел живого Олександреску и живого Walter Bright (который "D"). У них тут, сцуко, гнездо.
Отжыг назывался "Machine Architecture: Things Your Programming Language Never Told You" (здесь можно скачать презентацию и видео) и был про конкретную часть abstraction penalty - Latency.
Я попытаюсь коротко рассказать о ключевой мысли доклада. Она простая, очевидная и тысячу раз сказанная. Думаю, еще раз повторить азбуку - никогда не повредит.
Для самых маленьких, о том что такое Latency и Bandwidth.
Bandwidth - это ширина канала. Сколько можно прокачать данных за секунду, сколько можно пустить инструкций чтобы полностью загрузить ALU и так далее.
Latency - это длина канала, то есть через какое время к тебе придут данные, которые ты попросил. Через сколько тактов к тебе придет запрошенный бит из памяти, через сколько тактов будет готов результат инструкции, когда команда пройдет до конца пайплайна и так далее.
И они, разумеется, друг на друга влияют. Как только нужен результат, а делать больше нечего - весь bandwidth простаивает из-за latency. Запросили память, которой нет в кеше - сидим, ждем память. Захотели выполнить инструкцию, которой необходим результат предыдущей - ждем ее выполнения. Это создает "пузыри" в канале и соответственно уменьшает загрузку.
Херб в презентации использует пример нефтепровода, он вполне наглядный. Можно прокачивать дикое количество баррелей в минуту, но каждый баррель идет до места назначения несколько дней. В чистом виде bandwidth и latency.
Практически важный момент в том, что bandwidth всегда легко покупать. Поставить поставить два процессора, брать из памяти за раз в два раза больше данных, поставить два компьютера в конце концов. Latency же гораздо дороже - две женщины не родят ребенка за 4.5 месяцев, и продвигается оно только прогрессом - увеличивать частоты, уменьшать размеры элементов, менять технологию и так далее.
И вот последние 20 с лишним лет показывают, что latency улучшается гораздо медленней. Особенно - latency памяти.
Ща, у Херба там табличко была...
1980 VAX-11/750 Modern Desktop Improvement since 1980
Clockspeed (MHz) 6 3000 +500x
Memory size (RAM, MB) 2 2000 +1000x
Memory bandwidth (MB/s) 13 7000(read) +540x
2000(write) +150x
Memory latency (ns) 225 ~70 +3x
Memory latency (cycles) 1.4 210 -150x
|
| _Winnie C++ Colorizer |
Из таблички хорошо видно, что процессор хорошо растет, размер памяти хорошо растет, bandwidth памяти опять же зашибато, а вот latency со времен VAX - стало всего в три раза лучше. В расчете на такты (последняя строка) - ухудшилось в 150 раз.
Что означает, что промах кеша стоит на порядки больше даже самых тяжелых инструкций процессора.
В 80-х годах было просто и здорово - стоимость доступа к памяти была вполне сравнима, а то и меньше, вычислительных инструкций (а на floating point так и вообще),
Есть процессор, диск и память, программист ими непосредственно и оперирует. Код выполняется прозрачно и предсказуем до такта.
Сейчас же в железе на самом деле все по-другому. Доступ к памяти - сотни тактов. Да, за раз можно взять целый cache line (32 или 64 байта), но ждать все равно сотни тактов. В миллисекунду, например, получается обратиться в разные места памяти примерно 10000 раз. 100 объектов разных классов, вызов 10 виртуальных функций в каждом - уже 20+% от миллисекунды. В геймдеве - очень реальные цифры. А трафик памяти, вообще говоря, самое важное что у нас есть.
И это все про память. Если полезли к диску - это уже совсем за пределами добра и зла, там latency в десятки миллионов тактов.
Как это лечить - разумеется кешем и иерархией. L1 - 2 такта, L2 - 14 тактов, L3 - let's say about 40. Отдельно для данных, отдельно для инструкций.
Сложная логика кеша, ноу-хау различных производителей процессоров и прочее.
Кроме этого - обязательно out of order, чтобы пытаться выполнять то, что не зависит от ждущих.
Out of order execution, register renaming, обязательно мощный branch prediction, обязательно стартовать доступы и записи в память как можно раньше. Если бранч пойдет не в ту сторону, это сразу рушит out of order и является катастрофой.
Опять же, там внутри длинный конвейер. На P4 был даже патологически длинный - до 25 инструкций за раз и out of order заглядывал вперед на сотню. На последних процессорах конвеер меньше, но все равно непрозрачный.
Саттер пишет, что на Itanium2 кеш занимает 85% площади процессора.
На Core Duo - я не смог нагуглить, думаю примерно также.
Еще 10 с лишним процентов - логика out of order, branch prediction и прочего добра.
Остаются считанные проценты на собственно ALU, которые реально что-то считают.
Современный процессор - это не вычислитель, а гигантский хардверный эмулятор x86-инструкций.
Вся это нужно для того, чтобы спрятать от программиста latency. Чтобы можно было продолжать программировать в 80-х годах - когда есть только процессор и память, причем к памяти доступаться можно сколько угодно недорого. Чтобы продолжать запускать старый код все лучше, чтобы новый можно было писать также.
И все же - мы пытаемся скрыть падение скорости в 150 раз! Незаметно для программиста! Не изменяя его структур данных! Так, чтобы он не заметил изменения порядка выполнения инструкций!
Разумеется, это занятие никогда не будет оптимальным.
Из того что программист в некотором смысле живет в стране эльфов, Саттер делает два практических следствия.
Первое - это влияет на корректность программ.
Везде, где делаются предположения о последовательности чтений-записей в память, в любимой Саттером многопоточности.
Если, предполагая, что запись int в память атомарна, начать делать lock free взаимодействие тредов - ушибешься.
Например:
Thread1:
flag1 = 1;
if (flag2 != 0) { ...}
// enter critical section |
| _Winnie C++ Colorizer |
Thread2:
flag2 = 1;
if (flag1 != 0) { ...}
// enter critical section |
| _Winnie C++ Colorizer |
Тред1 сначала выставляет flag1 - флаг того, что он хочет shared resource, и проверяет не занят ли второй ресурс другим тредом. Делается предположение, что flag2 проверится только после установки flag1 (чтобы не войти в critical section если она занята другим тредом).
И будет тотальный превед - memory read на flag1 произойдет очень рано из-за out of order (формально, этот read ни от чего не зависит, поэтому его можно делать рано), и никакой синхронизации не будет.
Поэтому нужно честно локать. Полагаться на память как на что-то, что отражает значения переменных - нельзя.
Второе и самое веселое - конечно, производительность.
Уже давно в основном тормозит память. В основном из-за latency, а не bandwidth. Случайное чтение памяти - много дороже целой тучи вычислений. Locality matters, на всех масштабах.
Кстати, что такое "случайное" в реальной программе страшно размазывается из-за непрозрачной иерархии кешей.
Вроде бы если используется много - то и так будет в кеше. С другой стороны, сколько реальный working set в разные моменты - толком и не прикинуть.
А еще оно на каждом процессоре разное. А еще оно крайне зависит от данных. И самое классное - его еще и хрен померять!
Свел пример к синтетическому - он стал помещаться в кеш. Превед.
К счастью (к сожалению?), цена кеш-мисса столь велика, что серьезные проблемы можно померять и сквозь толстую прослойку.
Скорость random access (меряем latency) против sequential access (меряем bandwith) отличается на порядок. Это разница между std::vector vs std::list.
Хуже, это может быть разница между std::vector<Type> vs std::vector<Type*> (это, как все знают, и массив референсов в managed-коде).
В итоге - надо всегда думать о памяти. Как о локальности, так и о затратах.
Мерять, не в память ли уперся. Когда в random access - можно продуктивно думать и решать. И когда в footprint - бывает тоже.
Но точно померять и предсказать все равно не получается. Все очень толсто, нелинейно и непрозрачно. Под тобой работает большая машина с непонятной логикой и, что хуже, непонятной загрузкой. Оживет в бэкграунде сеть и все спутает. Или индексер, упаси господь.
И я не знаю, что с этим делать в PC-мире.
С одной стороны, хочется больше контроля. Иметь четкое место в кеше, где я могу иметь гарантированное время доступа. Иметь некие гарантии того, что мне не попортят кеш при первом же context switch.
Вот например, легко рассуждать о том, как хорошо все в консольном мире. SPU, 256 kb полностью управляемой очень быстрой локальной памяти, четкие запросы в основную память широкими (чтобы прятать latency) DMA-пакетами. Или Xbox360, где можно локнуть на время часть кеша, да еще и попросить GPU из него рендерять.
Ни одна из этих моделей не заживет на PC в чистом виде.
На одном процессоре живет множество тредов одновременно, если каждый будет управлять 256 килобайтами памяти, то при context switch ее всю надо выгрузить и загрузить. Будет тяжелый и долгий context switch, а типично в OS ну просто дофига даже полу-активных тредов.
Локать кеш нельзя позволять по тем же причинам - это означает либо буфферить его в память при context switch, либо забирать его навсегда от других приложений. Если забирать будут даже только активные - остальное станет тормозить.
Хуже, основные аппликейшены - без всяких верхних границ. Могут загрузить документ и в 10 килобайт, и в 100 мегабайт. Размер Excel-таблица может отличаться в тысячи раз, никаких верхних границ по памяти, как на консоли - не поставишь.
Причем и набор железа, и количество памяти всегда разное, таргет вязкий - "кушать памяти поменьше и работать побыстрее". И железо больше эмулирует, чем работает.
Жизнь одного аппликейшена в системе на фиксированном железе без обратной совместимости принципиально отличается от жизни тучи разношерстных на неопределенном железе, со старым кодом и другими требованиями. Чем дальше смотрю, тем больше думаю, что разные миры.
И это малая часть проблем. Я бы сказал, фундаментальные - backward compatibility и совсем другой чем в играх баланс "performance против стоимости разработки". Но об этом можно как-нибудь потом писать бесконечно много.
Напоследок, краткие медитативные цифирки (я брал у себя на домашней машине):
floating point mul: 0.5-4 cycles (на одном ядре)
L1 access (~16-32 kb) : ~2-3 cycles
L2 access (~2-4 mb) : ~15 cycles
Random Memory Access: ~200 cycles
Sequential Access with Prefetch: ~2 bytes/cycle
Остается бороться, мужики. Понимать цену абстракции и на этом уровне, не давать мозгам расслабляться и жить в восьмидесятых годах.
Crossposted to blog.gamedeff.com
UPD:
Must-read статья про то, как и почему работает память и что с этим делать - What every programmer should know about memory
На blog.ff Борис написал отличный пример того, как эти знания применять на практике.
October 9 2007, 07:14:58 UTC 4 years ago
2. [Error: Irreparable invalid markup ('
2. [Error: Irreparable invalid markup ('<type*>') in entry. Owner must fix manually. Raw contents below.]
October 9 2007, 07:18:11 UTC 4 years ago
Спасибо!
October 9 2007, 07:27:05 UTC 4 years ago
http://www.bookland.ru/book1418882.htm
October 9 2007, 07:28:30 UTC 4 years ago
4 years ago
4 years ago
4 years ago
October 10 2007, 12:38:32 UTC 4 years ago
October 9 2007, 07:36:26 UTC 4 years ago
После прочтения прочувствовал, что такое память и как с ней бороться. (А ведь кроме того, что "в кеше/не в кеше", играет роль, даже, к активной или к неактивной странице памяти мы обращаемся).
Как показали последующие эксперименты, за счет правильной/неправильной организации структур в памяти, можно легко получить выигрыш (или проигрыш) раз в пять.
PS. Эх, вот разгребусь с текущими делами на работе, и буду пробовать, что хорошего дают 8М L2 в Core2 Quad...
October 9 2007, 08:33:54 UTC 4 years ago
1. Сделать запись атомарной - это утопия и глупость, поэтому никто и не делает.
2. Отличная схема atomic swap как раз на твоем примере работает и отлично.
Есть огромнейший минусище на текущем железе. Почти все atomic operations стоят безумно дорого и порой лучше без них и гораздо быстрее единственный lock\cs
October 9 2007, 08:40:25 UTC 4 years ago
atomic operations именно потому и стоят дорого, что есть большой конвеер и большие латентности.
October 9 2007, 09:24:50 UTC 4 years ago
October 9 2007, 09:25:56 UTC 4 years ago
4 years ago
4 years ago
October 9 2007, 10:38:52 UTC 4 years ago
2) Я проводил эксперимент по выборке данных в сортированном порядке. Ускорение работы выборки при сортировке у меня получилось 1.5 раза для параллельного прохода по дереву.
Последнее может быть выполнено на Cell SPU и достаточно эффективно.
October 9 2007, 14:43:38 UTC 4 years ago
4 years ago
October 9 2007, 12:50:21 UTC 4 years ago
October 9 2007, 14:36:54 UTC 4 years ago
October 9 2007, 14:41:50 UTC 4 years ago
RTOS типично гораздо более управляемый, чем PC. Там таки лучше контролируют, что одновременно запускается.
Prefetch - бывает что спасает, и им обязательно надо пользоваться, смотри например ссылку на пример в конце поста. Но различие между локом кеша и префетчем - принципиальное. Есть гарантия или нет. Есть возможность обеспечить маленький лукап кешем или нет (префетчить его на каждой итерации же не будешь). И т.д.
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
October 9 2007, 18:50:21 UTC 4 years ago
Почитать текст со всеми комментариями можно тут
Это Ваш 1-й ТОПовый пост за последний год. Посмотреть статистику автора можно тут.
Этот "бот не имеет отношения к Яндексу" © НадежныйИсточник
October 9 2007, 21:44:08 UTC 4 years ago
October 9 2007, 21:50:51 UTC 4 years ago
4 years ago
October 9 2007, 22:50:14 UTC 4 years ago
PS: классный пост!
October 13 2007, 22:09:38 UTC 4 years ago
> latency растет гораздо медленней
latency не растёт, а падает :)
за context switch не бойся, L2 кэш успел бы приблизительно 100 раз заполниться между свитчами. Гораздо более страшная жопа -- это когда в SMP происходит запись в память, соответствующую линию кеша надо вытравить во всех процах системы
кто такой индексер? :)
пс: что станет с PC -- порассуждать интересно, есть идеи. Но тема отдельного поста :) Точно могу сказать, основные тренды архитектуры останутся ещё очень надолго :) придётся какие-то задачи объявить хорошими, а какие-то плохими
October 13 2007, 22:12:41 UTC 4 years ago
Да ну нафиг. Context switch - че-то типа 4 ns в лучшем случае, латенси памяти - 70 ns. То есть, даже положить данные в память не успеть. А куда если не в память?
Индексер - это indexer service. Который данные на диске в бэкграунде индексирует. Хоть наш, хоть ваш.
Порассуждай, почитаю :)
October 14 2007, 07:52:42 UTC 4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
October 25 2007, 08:43:06 UTC 4 years ago
Intel® 64 Architecture Memory Ordering White Paper
January 9 2008, 16:08:56 UTC 4 years ago
January 9 2008, 16:29:16 UTC 4 years ago
4 years ago