Комментарии 37
Производительность чтения-записи живьем не сравнивали? Когда нужна производительность (а производительность процессоров, в общем-то стоит на месте последние годы), си снова становится портативным ассемблером. Где важно было бы поймать за руку вылет за границы массива, давно были придуманы языки попроще. Имхо.
Однако, умножение этого числа на 2 ведет к ошибке, так как совершенно непонятно, что значит «умножить такой абстрактный указатель на 2».Честно говоря совершенно не тот пример. Проблема не в том, что указатель нельзя умножать, иногда может и можно, проблема в том, что у указателей есть обособленное значение null, эквивалентное NaN в числах с плавающей точкой. Строго говоря любые операции, включающие в себя null обязаны давать null в результате. Но поскольку null отображается на ноль, не имеющий в целых числах такой особенности и возникает большая часть той чехарды, что мы имеем.
Однако это значит, что простая операция вроде чтения байта из памяти не может просто вернуть u8.
Если честно совсем не понял почему. Может кто-нибудь пояснить?
Потому что в области памяти могут лежать указатели. И тогда u8 — это байт, который является частью указателя. И тут два пути, либо мы забиваем на то, что u8 — это часть указателя, что стирает инфу о хранимом указателе, либо надо надо каждый байт рассматривать как какое-то значение тип-суммы байта|части указателя. В этом же части статьи чуть ниже описано.
Ну то есть в чем разница, что мы попросили байт от указателя или от u32?
Половина поста посвящена объяснению этого. Указатель 0xFFFE на один массив и указатель 0xFFFE на другой массив — это разные указатели, даже если у них одно и то же байтовое значение.
Если язык будет "стирать" эту разницу (считая, что раз 0xFFFE, то и ладно), то либо будет сильно страдать оптимизация, либо будет много неприятного UB.
А если я скопирую 2 байта из одного указателя, а потом два байта из другого указателя, это какой именно указатель будет?
Это риторический вопрос, на самом деле. Для постороннего человека вся эта трахомудия с указателями ещё менее понятна, чем проблема поиска отображения из одного семигрупоида в другой.
Суть проблемы: если у нас есть указатель на что-то, и компилятор видит, что это указатель единственный, он может оптимизировать код. Например, если из этого указателя читают то, что записали, сравнивают со старым значением и делают if, то весь блок else можно выкинуть, потому что это тафтология.
А теперь кто-то рядом делает указатель на те же данные методом "кастим 8 байт и пишем туда что-то". И это был осмысленный код (с точки зрения того, кто пишет). Что компилятору делать? Три варианта:
- Запретить делать фигню с указателями. Мы не можем полностью это запретить, потому что кто-то должен обрабатывать прерывания и переключать real mode в 64-битный режим.
- Отключить все оптимизации делать что сказали. Работает (с поправкой на баги человеков), но меееедленно. А все, кто лезут в указатели, хотят быыыыстро.
- Объявить это undeined behaviour и делать что делается, и получится что получится. (Текущая ситуация с указателями).
Вот тут вот и пытаются принести здравый смысл в модель указателей без запрета оптимизаций.
Дело не в том, как эту область преобразовать в указатель, а в том, что наличие "неучтённых" указателей ломает оптимизации в других местах (не там, где указатели появляются).
Вся идея поста состоит в том, чтобы добавить к каждому байту (байту!) виртуальной машины низкоуровневого интерпретатора (точнее, эмулятора виртуальной машины) информацию о том, каким именно указателем он является, чтобы сохранить одновременно и здравый смысл и оптимизации.
В статье писали про интерпретатор, обратите внимание. Он такое может.
В языке Rust есть компонент под названием MIRI. Он нужен для интерпретации кода для поиска багов (UB) в рантайме, типа плюсовых санитайзеров, но со специализацией именно в Rust. Работа над ним все еще идет, но он уже нашел несколько багов в стандартной либе (в unsafe части). Ральф как раз и занимается формальной верификацией и MIRI, поэтому периодически выдает всякие статьи о том, как оно внутри устроено, как они хотят что-то улучшить и т.д. Советую почитать его блог.
Автор текста вполне донес до меня мысль, что указатель — это вещь в себе, и если они не указывают на одну и ту же область памяти, то с точки зрения компилятора это все равно что разные типы данных.
Вопрос в том, чем указатели отличаются в этом плане от других типов данных? Если взять любой тип данных и начать производить с ним любые побайтовые манипуляции кроме полного копирования, то скорее всего ничего хорошего не выйдет.
UPD. Я еще раз перечитал предыдущее объяснение и понял.
В какой-то момент времени это придётся делать, если спускаться вниз по стеку к более низкоуровневому коду. У нас может прийти сетевой пакет, в котором фрагментация, и граница режет наш любимый int не там, где он заканчивается. Прийдётся делать memcpy, а потом его оттуда вычитывать. Код вычитывания можно сколько угодно обложить красивой типизацией (я не кастю память к типу просто так), но внутри оно всё равно будет кастить память к типу просто так.
Если вы будете приходящие сетевые пакеты вычитывать по-байтно, а объединять сдвигами, то у вас будет непреодолимая проблема — сетевой пакет должен обрабатываться за 8нс (наносекунд) чтобы обеспечить linespeed на 40G. Это 200 пикосекунд на байт, что меньше времени выполнения одной простой инструкции на современных процессорах.
Особенно приятно при оптимизации становится простым тестам памяти и скрабберам.
Я не могу говорить про Си, а в Rust со скрабберами всё довольно просто, потому что трейт Copy надо явно иметь (иначе тебя нельзя копировать), а чистку надо делать в трейте Drop (потому что его drop() будет точно вызван точно тогда, когда объект выйдет из scope'а).
… Как эту проблему решают в Си — я представить себе не могу. Видимо, как-то решают.
А скрабберам-то что? Если виртуальная память используется, и область памяти с GDT не затронута, надо всего лишь ремапнуть страничку. Даже ссылки не поменяются (в userspace). В kernel — ой.
По моему пониманию, когда мы разыменовываем указатель, то мы никогда не получаем «сырые» байты. У указателя всегда есть какой-то тип, и при разыменовывании мы должны получить значение этого типа.
Потому что если сказать, что значение в памяти, на которое указывает указатель — это либо 8-битное число, либо какая-то часть указателя, то что делать со всеми остальными типами? Например, числа с плавающей запятой — точно так же, как и для указателей, любой один прочитанный байт никакого смысла не несет. И, в общем-то, это верно для любых многобайтовых типов.
Почему вместо хранения типа указателя (применимого для всех типов данных) в статье введен тип значения, хранящегося в байте?
Я бы сказал, что это для упрощения, но упрощения я здесь не вижу. В этом есть какой-то скрытый смысл?
Честно говоря, статья — хрень полнейшая.
То ли автор говорит про Strict Aliasing Rules, то ли про то что указатель это не значение адреса памяти, то ли про оптимизации…
А потом еще пишет: "Так что такое указатель? Я не знаю полный ответ."
На своей волне где-то там плавает...
то ли про то что указатель это не значение адреса памяти, то ли про оптимизации…
Потому что надо прочесть пейпер, который указан в статье http://www.cis.upenn.edu/%7Estevez/papers/KHM+15.pdf. Автор опирается на академические исследования. В этом же пейпере и говорится про указатель как инт или указатель как модель, и как это применять в оптимизациях.
Статья — похожа на крик души человека, который занимается компилятором или анализатором rust. Я так не понял, что сделает компилятор c++ и rust, когда ему передадут два объекта одинакового типа. Вроде в c++ отключают какие-то оптимизации если нельзя доказать что объекты разные.
В Rust возможны варианты. Например, если функция принимает параметры a: &mut T и b: &T, то Rust совершенно однозначно может сказать, что это два разных объекта; стало быть, вердикт — NoAlias. Если a: &mut T и b: &mut T то тоже NoAlias. И только если обе ссылки &T то MayAlias.
Результат проведения такого анализа использует LLVM для того чтобы, например, проводить оптимизации redundant load/store elimination, тем самым повышая производительность.
указатели просты: они являются простыми числами
С учетом выравнивания, указатели в принципе не могут быть простыми числами, так как выравнивание всегда происходит по кратному некоторого количества единиц (байтов). Так что переводите аккуратнее (здесь бы больше подошел перевод "они являются просто числами").
Вообще, правильно будет сказать, что указатели — это не количественные (т.е. которые отвечают на вопрос "сколько"), а порядковые (вопрос "какой по счету") числа. Хороший пример "из реальной жизни" — это календарь (например, годы — но подойдут так же и века или числа в месяце, например). По сути, с ними можно проделывать те же самые арифметические операции, что и с указателями: прибавлять и вычитать к ним "обычные" числа (2019-10=2009, 2019+10=2029; то есть, если сейчас 2019 год, то 10 лет назад был 2009, а через 10 лет наступит 2029-й), вычитать друг из друга (2019-1942=73; один из авторов языка Си, Брайан Керниган, родился в 1942 году, значит, в 2019 ему 73 года), причем в рамках единого "адресного пространства" (смысла нет, например, вычитать год по хиджре из григорианского года). Но никто в здравом уме не будет складывать годы друг с другом (2019+1942=3961 — какой в этом смысл (конечно, если 1942 — это тоже порядковый номер года, а не число лет)?) или умножать их на числа (2019*3=6057 — то же самое, особого смысла нет).
Да, спасибо. Поправил.
Тут действительно имеются ввиду не простые числа (2, 3, 5, 7 и т.д.). "Просто числа" звучит однозначнее.
Но никто в здравом уме не будет складывать годы друг с другом (2019+1942=3961 — какой в этом смысл (конечно, если 1942 — это тоже порядковый номер года, а не число лет)?) или умножать их на числа (2019*3=6057 — то же самое, особого смысла нет).
Но если мы хотим найти середину отрезка между 1942 и 2019, то нам придется как складывать порядковые числа, так и умножать их на скаляр: (1942 + 2019) * 0.5, при этом результат будет вполне осмысленным.
Указатели сложны, или Что хранится в байте?