Всё-таки память под thread-local данные выделяется не там же, где выделяется память под разделяемые данные. Например, на стеке не будет храниться разделяемых данных, а операции на стеке выполняются сильно чаще, чем в разделяемой памяти. Соответственно, обеспечивать когерентность для стека не только бессмысленно, но и тяжело с точки зрения затрат ресурсов.
Что касается прикладного программиста, ему и не нужно знать размер страниц. Ему нужен только механизм выделения памяти с описанием того, как он эту память собирается разделять между потоками. Всё остальное — задача операционки.
Отдельного комментария заслуживает часть про когерентность кэша. То, что вы написали, справедливо для x86, однако, например, в ARM каждая страница памяти может быть в таблицах страниц помечена как Non-shareable, Inner Shareable или Outer Shareable. Соответственно, когерентность для Non-shareable памяти в ARM не обеспечивается, а для Inner Shareable памяти обеспечивается только внутри Inner Shareable-домена, куда обычно входят только процессорные ядра, но не входит периферия, которая может производить DMA-запросы.
Таблицы страниц — это полностью задача операционки. И WXN может включать и выключать только операционка.
Но на ARM любой self-modified code работает через кучу манипуляций. Instruction Cache в ARM не является когерентным остальной памяти, поэтому прежде чем исполнять код, который только что был записан в память, необходимо сделать Data Cache Clean и Instruction Cache Invalidate для всех cachelines, в которых находится модифицированный код. Если эту процедуру не выполнить, результат непредсказуем. При этом, в 32-битном ARM Cache Maintenance операции доступны только операционке. В AArch64 эти операции стали доступны и в application mode, но операционке может их там запретить.
Вообще, в таблицах страниц MMU современных архитектур (x86-64, ARM) присутствует бит запрета исполнения (eXecute Never, eXecute Disable). И этот бит кэшируется в TLB. Попытка исполнить код из страницы с выставленным битом приводит к page fault exception. Кроме того, в ARM есть режим Write-eXecute Never, в котором нельзя исполнять код из страниц, доступных для записи.
Посмотрите документ «Intel® 64 and IA-32 Architectures Software Developer’s Manual», Volume 2, Chapter 4, раздел «4.3», описание инструкции «MOVS/MOVSB/MOVSW/MOVSD/MOVSQ—Move Data from String to String». Документ можно скачать здесь: https://software.intel.com/en-us/articles/intel-sdm
По поводу доли на рынке: мир не ограничен x86 и десктопами. В телефонах, например, ARM. А в сетевое оборудование часто ставят MIPS. А в серверах можно и SPARC найти, правда тяжеловато. И что, на них C++ не использовать?
Ещё следует упомянуть о флагах. В x86 и amd64 поддерживается один набор флагов, в ARMv7/v8 другой, в MIPS вообще флагов нет, а целочисленное переполнение на signed сложении генерирует исключение. И семантика у операций может быть разная: например в x86 инструкция SUB устанавливает Carry Flag, если был заём. А в ARM логика противоположная, Carry Flag выставляется, если заёма не было.
А вы не думали, как Ваше предложение скомпилируется, скажем, для процессора архитектуры MIPS, где нет флагового регистра и операции AddWithCarry? Боюсь, работать не будет
Увы, но C++-код должен компилироваться на множестве аппаратных платформ. И на многих из них нет операций типа сложения с переносом, SIMD и прочих. Поэтому сомневаюсь, что кто-то решит затащить эти операции в стандарт языка — они очень платформозависимы. А если они кому нужны на конкретной платформе, народ использует intrisincs.
Что касается «платформонезависимых ассемблерных вставок», хотелось посмотреть, как вы себе это представляете.
Для генерации вещественных чисел, как мне кажется, имеет смысл посмотреть на алгоритмы из Berkley Testfloat. http://www.jhauser.us/arithmetic/TestFloat.html
Почему был выбран именно такой метод генерации случайных float и double? Правильно ли я понимаю, что библиотека не вернёт NaN в качестве случайного float или double? Также, как мне кажется, крайне мала вероятность получить subnormal или бесконечность.
Ещё раз: если вы попали в метод экземпляра класса, то this != nullptr. Так должно быть по стандарту. И компилятор имеет право генерировать код исходя из этого условия. Если программист допустил преобразование nullptr к указателю на объект, а потом вызвал метод у этого объекта, то этот программист сам себе злобный Буратина.
А отлавливать такие вещи можно если gcc подать опцию -fsanitize=undefined. Это заставляет компилятор вставлять рантайм-проверки во все места, где может возможно undefined behavior. Естественно, это имеет смысл применять только в дебажных сборках.
В этом случае проверка на равенство nullptr имеет право даже не выполняться. Если мы вызвали метод экземпляра класса, считается, что указатель p содержит валидный адрес обьекта. Правильно было писать так:
class foo {
public:
void bar() {}
};
int main() {
foo *p = nullptr;
if (p != nullptr)
p->bar();
}
В вашем примере необязательно проверять указатели на равенство nullptr в обработке исключений — оператор delete должен работать корректно, если ему передадут nullptr, то есть ничего не делать.
Не следует писать код, приводящий к undefined behavior. Собственно, в чём смысл этой статьи? Показать какой GCC плохой — считает что в пользовательском коде никогда не встретится UB? Ну так он по стандарту должен это делать.
Кстати, какая альтернативно одарённая личность могла догадаться использовать в продакшене GCC с минорным номером версии меньше двойки?
Что касается прикладного программиста, ему и не нужно знать размер страниц. Ему нужен только механизм выделения памяти с описанием того, как он эту память собирается разделять между потоками. Всё остальное — задача операционки.
Но на ARM любой self-modified code работает через кучу манипуляций. Instruction Cache в ARM не является когерентным остальной памяти, поэтому прежде чем исполнять код, который только что был записан в память, необходимо сделать Data Cache Clean и Instruction Cache Invalidate для всех cachelines, в которых находится модифицированный код. Если эту процедуру не выполнить, результат непредсказуем. При этом, в 32-битном ARM Cache Maintenance операции доступны только операционке. В AArch64 эти операции стали доступны и в application mode, но операционке может их там запретить.
Что касается «платформонезависимых ассемблерных вставок», хотелось посмотреть, как вы себе это представляете.
А отлавливать такие вещи можно если gcc подать опцию -fsanitize=undefined. Это заставляет компилятор вставлять рантайм-проверки во все места, где может возможно undefined behavior. Естественно, это имеет смысл применять только в дебажных сборках.
В этом случае проверка на равенство nullptr имеет право даже не выполняться. Если мы вызвали метод экземпляра класса, считается, что указатель p содержит валидный адрес обьекта. Правильно было писать так:
Кстати, какая альтернативно одарённая личность могла догадаться использовать в продакшене GCC с минорным номером версии меньше двойки?