Pull to refresh

Comments 16

Спасибо за обзор. Подскажите, а есть ли готовый анализатор для встраиваемых систем? Иногда надо понять, успеет ли микроконтроллер прожевать прерывание, или стоит ограничиться только установкой флага и обрабатывать в основном цикле.
Имхо, запись в память все же не настолько бесплатна.
Да, проц может её делать в бекграунде, но для этого придется считать линию в кеш (предварительно вытеснив что-то из него), обновить её и записать обратно. Т.е. как минимум какие-то данные из кеша будут вытеснены почем зря. А на NUMA системе записываемая линия будет ещё и инвалидирована на всех нодах.
Понятное дело, что если данные надо сохранить, то писать в память придется, хочется того или нет. Но иногда бывают случаи, что надо установить флаг, например, — если это надо делать очень часто, то лучше проверить текущее состояние флага и записывать в память только тогда, когда старое значение отличается от нового.
Это из серии «Тормозите лучше в папу. Папа мягкий. Он простит.»

Да, теоретически можно придумать ситуацию, когда вы пишите флаг часто, реально меняете его редко, у вас при этом большая NUMA-система и всё такое прочее… но это всё жуткая экзотика.

На практике вы линию из кеша не занимаете, она вначале во write combining buffer попадает. Кстати именно из-за write combining зачастую предлагаемый вами алгоритм будет скорее пессимизацией. Почитайте на досуге.
придется считать линию в кеш (предварительно вытеснив что-то из него), обновить её и записать обратно. Т.е. как минимум какие-то данные из кеша будут вытеснены почем зря.

А разве нет инструкции для записи в память минуя кеш не вытесняя при этом из него другие данные? Насколько я помню для чтения есть инструкции (non-temporal) которые позволяют считать из памяти в регистры не кешируя (на случай если мы хотим один раз вычитать что-то не вытеснив при этом более важные оперативные данные из кеша). Есть ли что-то подобное для записи?

Есть и на чтение, и на запись. Но сценарий это довольно редкий, и чтобы его использовать, должны быть очень веские основания.

Касательно CAS: 10-15 тактов — это действительно оптимистичная ситуация, когда только одно ядро лезет в память. Когда несколько ядер теребят одну и ту же переменную, задержки значительно возрастают: помимо того, что ядра становятся в очередь, так и ещё и шина данных начинает ругаться в стиле «вас много, а я одна» и замедляет обслуживание.

Делал как-то тест: запускал потоки, которые в бесконечном цикле увеличивали переменную, а зачем через секунду смотрел её значение. Результаты для процессора Core i7-2600K 4 GHz, 4 cores / 4 threads (HT off): 1 ядро — 22 такта на инкремент, 2 ядра и выше — 61*N тактов на инкремент, где N — число потоков.
Примечание: если зафиксировать affinity до 1 ядра, то выходит 13 такта на инкремент.

Поэтому когда я смотрю на совеременные 32-поточные процессоры, становится немного по себе от факта, что накладные расходы на синхронизацию могут превысить выгоду от распараллеливания.
Поэтому когда я смотрю на совеременные 32-поточные процессоры, становится немного по себе от факта, что накладные расходы на синхронизацию могут превысить выгоду от распараллеливания.
Алгоритмы обработки нужно изначально под это затачивать. «Небольшую» параллелизацию лучше обеспечивают всякие SSE/AVX/AVX2/AVX512…
> Алгоритмы обработки нужно изначально под это затачивать. «Небольшую» параллелизацию лучше обеспечивают всякие SSE/AVX/AVX2/AVX512…

А что мешает использовать и параллелизацию, и векторизацию?
Просто указываю на то, что при большом количестве ядер гранулярность должна быть ниже, чтобы избежать больших расходов на CAS-операции.
поэтому на всяких Phi, даже в сильно-параллельных задачах и пр. приходится от нее отказываться, т.к atomic операции тормозят.

Насчёт правильных переходов — в Golang принято после каждого шага проверять if err != nil, и обычно, конечно, ошибки нет, потому условие не выполняется. Получается это плохо влияет на возможности по предсказанию? Или ЦП подстроится сам?

Про предсказание ветвлений есть отдельная статья.

Подстроится — но всё-таки это не совсем бесплатно. На очень простых функциях может быть заметно, но даже там — потери на фоне, собственно, стоимости вызова функции — копейки…
Интересно, как это:

Так же как и для любого типа кэша, существует стоимость промаха TLB; для x64 она колеблется между 7-21 тактами ЦП [7cpu].


согласуется с этим:

Однако, что сложнее найти, так это информацию о времени доступа к основной ОЗУ. [Levinthal] оценивает его в 60нс (~ 180 тактов, если процессор работает на частоте 3ГГц).


ведь промах TLB ведёт к обращению к страничным таблицам, а обращений этих может быть до 5 на современных x64 (без виртуализации, с виртуализацией — до 25), и адрес каждого обращения получается из предыдущего, т.е. их нельзя распараллелить.
Типичная ёмкость TLB-кеша в современных CPU — это всего-навсего 128 записей. В идеальном случае при 4K страницах этого хватает на полмегабайта, а если данные расположены «неплотно», то и ещё на меньшие обьёмы (вплоть до «предельно паталогического» случая, когда 129 обращений к 129 байтам приводят к тому, что TLB-кеш постоянно перезагружается). А ёмкости L3 кеша — существенно больше. Так что ходить в ОЗУ при промахе не приходится.

Кроме того совершенно неясно с какого перепугу вы решили, что если информация о конкретной странице будет выгружена из TLB кеша, то и информация о каталогах и прочем тоже будет выгружена. Не будет.

То есть TLB-промах, как правило, — это примерно один запрос в L3 (а не в оперативную память)… ну и дополнотильные пляски с интерпретацией этой информации…

Конечно если вы весь TLB-кеш сбросите — там уже другие числа будут…
совершенно неясно с какого перепугу вы решили, что если информация о конкретной странице будет выгружена из TLB кеша, то и информация о каталогах и прочем тоже будет выгружена. Не будет.


А она вообще там есть? Т.е. помимо записей вида (например, для 32-битных 4К страниц) 0xvvvvvxxx -> 0xpppppxxx в TLB хранятся записи вида 0xvvxxxzzz -> 0xppxxxzzz для каталогов (и они отличны от записей для страниц с адресами 0xvvxxxzzz)?
А она вообще там есть?

Отвечу сам себе: есть, но не в TLB. Документация intel выделяет эту информацию в отдельные структуры, называемые paging structure caches.
Для наших целей, говоря об операциях ALU, мы будем рассматривать только операции типа регистр-регистр. Если задействована память, затраты могут быть ОЧЕНЬ разными — это будет зависеть от того, “насколько велик был промах кэша” при доступе к памяти, как описано ниже.

Нужно делать не так, а рассматривать именно операции для которых доступны аргументы. А то, что записало туда эти аргументы — это совершенно другая история.

В наши дни (и на современных процессорах), «простые» операции, такие как ADD/MOV/OR/..., могут легко выполняться быстрее одного такта ЦП.

Не могут. То, что указанно в табличке за 0.5 и менее — это не время выполнение операции, а просто ipc(для одних и тех же операций). Время же выполнения самое операции — написано с столбике «летенси».

оценивает стоимость 32/64-битного умножения (MUL/IMUL в мире x86/x64) в 1-7 тактов

Не оценивает.

на практике я наблюдал более узкий диапазон значений, например 3-6 тактов

Это уже похоже на правду — по ссылки минимальная летенси 3такта( по крайней мере меньше я не увидел).

Если использовать скалярные SSE-операции (которыми, по-видимому, пользуется “каждая собака” в наши дни),

Действительно, х87 ещё с core2 выкинули на помойку.

показатели уменьшаться до 0,5-5 тактов для умножения (MULSS/MULSD)

Не уменьшатся. Как я уже говорил — время выполнение одно и то же и написано в столбике «летенси».

оценивает целочисленное сложение над 128-битным вектором в < 1 такт

Нет. Проблема та же, что и выше.

Не такая очевидная вещь, связанная с затратами на вычисления, заключается в том, что переключение между целыми и плавающими инструкциями не бесплатно. [Agner3] оценивает эту стоимость (известную как «задержка перехода») в 0-3 такта в зависимости от процессора. На самом деле проблема более общая, и (в зависимости от ЦП) также могут быть штрафы за переключение между векторными (SSE) целочисленными инструкциями и обычными (скалярными) целочисленными инструкциями.

Вот именно, что «задержка» и на те 0.5 — она «никак» не повлияет.

Совет по оптимизации: в коде, для которого критична производительность, избегайте комбинирования вычислений с плавающей запятой и целыми числами.

Совет не имеет смысла. Всё зависит от того, что понимать под производительностью и в подавляющем большинстве случаев под нею понимается не то, на что влияют эти задержки.

если процессор правильно угадывает, куда будет направлено выполнение (это до фактического вычисления условия if), тогда стоимость перехода составляет около 1-2 тактов ЦП

Из этого мало что следует, т.к. процессор умеет спекулятивно исполнять угаданную ветку. В любом случае — измерение этого «наобум» имеет мало смысла, т.к. в одном случае это может быть ноль, а в другом далеко не ноль. И всё это в рамках угаданного перехода.

процессору не нужно ждать завершения записи перед тем, как идти вперед (вместо этого он только начинает писать — и сразу переходит к другим делам).

Не совсем верно. Это лишь «размазывает» по времени редкие записи. Не нужно ждать пока не переполнился буфер/кэш, а далее ждать нужно.

Это означает, что большую часть времени процессор может выполнять запись в 1 такт; это согласуется с моим опытом и, по-видимому, достаточно хорошо коррелирует с [Agner4].

Не может. Есть два кейса — пишешь в рамках ширины буфера/кеша/etc — получаешь ~трупут буфера/кеша/etc, а вот если не пишешь — не получаешь. Ничего другого нет и быть не может. Всё так же, как для чтения. Хотя есть нюансы и они не в пользу записи.

Разница между чтением и записью в том, что чтение всегда является зависимостью для следующей, а запись нет. И вся задержка идёт именно в чтение.

Так же как и для любого типа кэша, существует стоимость промаха TLB; для x64 она колеблется между 7-21 тактами ЦП [7cpu].

Не совсем верно, а вернее совсем неверно. Стоимость промаха кэша — это не оверхед самого кеша( Это та задержка, которая существует между получением данным и возвратам их кешом. Это задержка на запись данных с вам кеш + много организационных задержек( надо узнать в какое место писать и прочее)). И по ссылкам это именно оверхед кеша.

Но ещё одно и основной свойство промаха в том, что надо прочитать данные из предыдущего уровня памяти и это и есть основная задержка, которую все и понимают под ценой промаха. И она в ссылке не указана, а вернее указана ниже. И там уже далеко не 10тактов. Ну и к тому же, бывают не только минорные пейджфолты.

еще раз — улучшение общей локальности памяти помогает уменьшить промахи TLB; чем локальнее ваши данные, тем меньше шансов выйти из TLB.

Странная формулировка с общей локальностью. Из TLB можно либо выйти, либо не выйти — третьего не дано. А выйти( 1к * 4к страниц в дефолте) из него не составляет труда.

С другой стороны, по моему опыту, для функции с достаточно небольшим числом параметров это скорее будет 15-30 тактов

Из этого так же мало что следует.

В то время как переключение между уровнями процессора занимает всего около 100 тактов, другие связанные с этим накладные расходы, как правило, делают вызовы ядра намного более дорогими, поэтому обычный вызов ядра занимает не менее 1000-1500 тактов процессора

Мало коррелирует с реальность для того же Linux.

0 тактов означает, что MOV не имеет дополнительную стоимость, поскольку она выполняется параллельно с некоторыми другими операциями

Не совсем верно — mov попросту не существует по причине отсутствия самих регистров. А даже если бы существовало, то не параллельно, а вместе.

В принципе, стоимость возврата-и-проверки состоит из трех отдельных стоимостей.

Куда делась стоимость самой проверки?

Объединив все три стоимости, я бы оценил затраты на возврат-кода-ошибки-и-проверку (в нормальном случае) в пределах от 1 до 7 тактов ЦП.

От нуля.

Резюмируя, перед написанием подобных статей — надо для себя и аудитории определить базовые понятия. В частности, что такое latency/throughput, почему и откуда они взялись. И почему вообще ничего, без контекста, предсказать нельзя.

Sign up to leave a comment.