Как стать автором
Обновить

Комментарии 25

Тонкая, однако, работа…

Exec. Отличает исполняемый код от данных. Используется для того, чтобы избежать исполнения неисполнимого. 1 разряд.

А чем не угодила современная система, где, если не ошибаюсь, под код отмечается сегмент? Код не данные, там высокая точность разметки не нужна. Смешивать же код и данные особого смысла вроде нет.
Код и данные смешивают, например, в JIT.
Про сегменты есть тонкости, если не ошибаюсь. При загрузке кода нет возможности (слишком дорого) проверять настройки сегмента, а в TLB для страницы нет такой информации.
Вообще, в таблицах страниц MMU современных архитектур (x86-64, ARM) присутствует бит запрета исполнения (eXecute Never, eXecute Disable). И этот бит кэшируется в TLB. Попытка исполнить код из страницы с выставленным битом приводит к page fault exception. Кроме того, в ARM есть режим Write-eXecute Never, в котором нельзя исполнять код из страниц, доступных для записи.
Спасибо за поправку. А как они выкручиваются с JIT — компиляторами?
На ARM для этого разрешают запись и исполнение одновременно, и не включают WXN. Но на ARM self-modified code вообще отдельная история
И всё это доступно из пользовательского режима?
Таблицы страниц — это полностью задача операционки. И WXN может включать и выключать только операционка.
Но на ARM любой self-modified code работает через кучу манипуляций. Instruction Cache в ARM не является когерентным остальной памяти, поэтому прежде чем исполнять код, который только что был записан в память, необходимо сделать Data Cache Clean и Instruction Cache Invalidate для всех cachelines, в которых находится модифицированный код. Если эту процедуру не выполнить, результат непредсказуем. При этом, в 32-битном ARM Cache Maintenance операции доступны только операционке. В AArch64 эти операции стали доступны и в application mode, но операционке может их там запретить.
На MIPS атрибутами страниц является как признаки данные/код, так и атрибуты кэшируемости и когерентности:

См. напр. упоминание на слайдах 92, 99, 200 в

http://www.panchul.com/dropbox/2013_11_23/imgtec_mips_microchip_masters_russia_2013.pdf



Кстати, в определенных микроархитектурных реализациях MIPS есть еще и дополнительные данные, хранящиеся в тэгах строк кэша. Эти данные называется precodes и они используются для помощи префетчеру+предсказателю переходов.
Ну, MIPS — вообще интересная архитектура. Там аппаратный Translation Table Walk является сильно опциональным с точки зрения архитектуры, если я не ошибаюсь.
Отдельного комментария заслуживает часть про когерентность кэша. То, что вы написали, справедливо для x86, однако, например, в ARM каждая страница памяти может быть в таблицах страниц помечена как Non-shareable, Inner Shareable или Outer Shareable. Соответственно, когерентность для Non-shareable памяти в ARM не обеспечивается, а для Inner Shareable памяти обеспечивается только внутри Inner Shareable-домена, куда обычно входят только процессорные ядра, но не входит периферия, которая может производить DMA-запросы.
Есть всё же разница.
Строка кэша — 64 байта, если я хочу покрыть слово, я готов мириться с тем, что под раздачу попадёт вся строка.
Страница в 4К — это уже перебор, как мне кажется, бОльшие страницы — в мегабайт и больше — совершенно непригодны для этого.

Кроме того, память выделяет runtime, я как программист понятия не имею на каких страница какого размера расположены данные. И знать не хочу.
Если у программиста будет возможность просто сказать volatile или std::atomic и гарантировать когерентность именно этих данных, это благо.

Всё-таки память под thread-local данные выделяется не там же, где выделяется память под разделяемые данные. Например, на стеке не будет храниться разделяемых данных, а операции на стеке выполняются сильно чаще, чем в разделяемой памяти. Соответственно, обеспечивать когерентность для стека не только бессмысленно, но и тяжело с точки зрения затрат ресурсов.

Что касается прикладного программиста, ему и не нужно знать размер страниц. Ему нужен только механизм выделения памяти с описанием того, как он эту память собирается разделять между потоками. Всё остальное — задача операционки.
Первый раз слышу, что память под стек выделяется каким-то особым образом.
Не могли бы привести ссылку в подтверждение?

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

Пусть программист вызывает malloc(100) и хочет чтобы эта память была когерентной.
Кому он что должен сказать? У Runtime нет информации о типе физических страниц, которыми ему нарезали память, может они по 4К, а может и по 1Мб. Значит на ровном месте появляется системный вызов, в котором на страницу, какой бы она ни была накладываются ограничения.

Тут вот какие возражения:
1) системный вызов там где он не нужен
2) наложение ограничений на потенциально большую страницу
3) разные ограничения могут противоречить друг другу

Механизм с тегами
1) всё делает в пользовательском режиме
2) ограничения с точностью до строки
3) когерентность всё равно идёт с точностью до строки
Что касается того, с какими опциями выделяется память под стек — не знаю наверняка. Я просто предположил, что нет смысла делать её разделяемой, к ней всё равно только один поток обращаться будет.

По крайней мере в *nix, память выделяется с помощью системного вызова mmap (http://man7.org/linux/man-pages/man2/mmap.2.html). Этот системный вызов принимает в том числе флаги, например MAP_SHARED или MAP_PRIVATE. Таким образом, операционка узнаёт, нужна потоку разделяемая память, или локальная. malloc — это библиотечная функция, которая вызывает mmap с какими-то дефолтными параметрами. Соответственно, если прикладного программиста не устраивают дефолты, с которыми вызывается mmap внутри malloc, он волен вызвать mmap самостоятельно. Также при вызове mmap нужно отдельно запрашивать выделение памяти большими страницами — для этого есть флаг MAP_HUGETLB.

Поэтому не вижу особых преимуществ тегов перед Shareability-атрибутом в таблице страниц. Для обоих механизмов нужна поддержка как операционки, так и приложения. При этом накладные расходы на хранение тегов гораздо больше, чем для атрибутов страницы. Собственно, может быть, хранить тег для каждого cacheline и удобно для прикладного программиста, но очень уж дорого. Да и размер cacheline вы в общем случае не знаете.
Упс, mmap проецирует содержимое устройства/файла в виртуальное адресное пространство.

Конечно, программист волен сам создавать сегменты. Но это:
1) дорого, для того и нужен runtime, чтобы буферизировать память.
2) неудобно, целых 2 механизма аллокации, кабы путаницы не вышло
3) загаживается TLB
При подаче флага MAP_ANONYMOUS никакой файл в память не отображается — отображается только физическая память в виртуальное адресное пространство. А про загаживание TLB не очень понимаю, что вы хотите этим сказать. В TLB и так хранятся атрибуты памяти: Memory Type, Cacheability и Shareability.
Имелось ввиду, что под каждый отдельный разделяемый кусочек придется выделять сегмент, что «загаживает» TLB.

Либо иметь специальный второй аллокатор. Что не очень удобно технологически. Например, делает невозможным совмещение в одном объекте когерентных и локальных данных.

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

Про второй аллокатор и совмещение в одном объекте когерентных и некогерентных данных, не понимаю, что вы имеете в виду.

У варианта с атрибутом в таблице страниц есть одно неоспоримое преимущество: он уже реализован в железе, причём не на экзотических архитектурах, а на вполне мэйнстримном ARM. Решение, работающее здесь и сейчас, на мой взгляд, гораздо лучше решения, которое ещё нужно сделать, причём не очень понятно, какие ресурсы для этого потребуются, и какой профит оно принесёт.
промахнулся, ниже ответил
Если для каждого мелкого кусочка вызывать mmap, виртуальных страниц будет очень много.
Поэтому нужна вторая куча под разделяемые данные.

Про совмещение когерентных и локальных данных.
Допустим, в варианте с тегами у меня есть структура
struct a { volatile char shared[64]; char local[64];};
компилятор в конструкторе проставит теги (в С это должен сделать программист).
При двух аллокаторах так сделать нельзя, требуется «char *shared», косвенные обращения и дополнительные вызовы alloc|free, что неудобно и неэффективно.

Конечно, вариант «здесь и сейчас прямо под рукой» имеет преимущества.
С другой стороны, зачем тогда Arm64 разрабатывали, чего он умеет такого, чего принципиально нельзя сделать, например, на MIPS64?

Страниц и так будет много — по одной на каждые 4k виртуального адресного пространства. Никаких особых проблем с поддержанием второй кучи я не вижу.

Что касается совмещения local и shared данных в одном объекте, пример какой-то искусственный. не очень понимаю сферу применения. Размер в 64 байта опять же с потолка взят — никто не гарантирует, что cacheline будет именно такого размера. Это implementation defined, от конкретной модели процессора зависит.

По поводу MIPS64 — не знаком с этой архитектурой, поэтому не готов комментировать отличий от ARM64. Что же касается причин разработки ARM64, тут, как мне кажется, всё довольно прямолинейно: ARM Limited хочет продать больше процессоров (на самом деле IP, но не суть), поэтому вынуждена разрабатывать что-то новое постоянно, иначе покупать перестанут.
В MIPS, например, страница может иметь размеры 4KB, 16KB,… 64MB.
В X86-64 тоже бывают 2-мегабайтные страницы.

Про пример, конечно он искусственный, но это не отменяет того факта, что прямой доступ к данным быстрее косвенного через указатель. И что дополнительные обращения к кучу — дополнительная сериализация.

Да я и не против ни Arm-а ни MIPS-а, «пусть расцветают сто цветов».
Мотивация же может быть отличной от «а то перестанут покупать».
Вот есть такая идея, она довольно изящно решает проблемы, штатные решения которых выглядят натянуто (всё IMHO, конечно). Я открыто описал, вдруг кому интересно.

Еще вопрос возник, а как Arm64 поступает с некогерентными данными при потере контекста?

В ARM64 с размерами страниц ситуация следующая: все размеры страниц зависят от базовой гранулярности, которая может быть 4k, 16k и 64k (конфигурируется). При гранулярности 4k размеры страниц 4k, 2M, 1G. При 16k — 16k, 32M. При 64k — 64k, 512M. Также существует Contiguous hint, позволяющий объединять несколько страниц в одну запись в TLB. Для 4k объединяется 16 страниц, для 64k — 32 страницы, а для 16k — 32 страницы 32M или 128 страниц 16k.

По поводу решения с тегами, я ведь не против, если кто-то сделает это в железе. Только пока такое железо если и есть, его не достать. Поэтому пока используем то, что есть.

Что касается non-shareable данных в ARM, проще, наверное процитировать документацию (ARM® Architecture Reference Manual. ARMv8, for ARMv8-A architecture profile. Ревизия B.a, страница B2-109)
For Normal memory locations, the Non-shareable attribute identifies Normal memory that is likely to be accessed only by a single PE.

A location in Normal memory with the Non-shareable attribute does not require the hardware to make data accesses by different observers coherent, unless the memory is Non-cacheable. For a Non-shareable location, if other observers share the memory system, software must use cache maintenance instructions, if the presence of caches might lead to coherency issues when communicating between the observers. This cache maintenance requirement is in addition to the barrier operations that are required to ensure memory ordering.

Как видно из процитированного, если софт (операционка, приложение) собирается использовать non-shareable данные с разных ядер, нужно самостоятельно выполнять Cache Maintenance. Это же нужно делать, если поток мигрирует на другое ядро. В случае же, если исполнение потока прерывается, а через какое-то время продолжается на том же процессорном ядре, никаких дополнительных телодвижений не требуется.
С аппаратными тегами ещё CHERI есть.
Спасибо за ссылку.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории