Pull to refresh

Comments 37

Выравнивание на 32 бит, в принципе, норма еще с 386. Так, что байт — байтом, но под капотом все существенно сложнее.
Производительность чтения-записи живьем не сравнивали? Когда нужна производительность (а производительность процессоров, в общем-то стоит на месте последние годы), си снова становится портативным ассемблером. Где важно было бы поймать за руку вылет за границы массива, давно были придуманы языки попроще. Имхо.

Мне трудно понять смысл вашего послания, но я попробую: работа с массивами в Rust медленее, чем в Си? Это не так. А пост касался вообще модели памяти, а не работы с массивами.

Однако, умножение этого числа на 2 ведет к ошибке, так как совершенно непонятно, что значит «умножить такой абстрактный указатель на 2».
Честно говоря совершенно не тот пример. Проблема не в том, что указатель нельзя умножать, иногда может и можно, проблема в том, что у указателей есть обособленное значение null, эквивалентное NaN в числах с плавающей точкой. Строго говоря любые операции, включающие в себя null обязаны давать null в результате. Но поскольку null отображается на ноль, не имеющий в целых числах такой особенности и возникает большая часть той чехарды, что мы имеем.
Однако это значит, что простая операция вроде чтения байта из памяти не может просто вернуть u8.

Если честно совсем не понял почему. Может кто-нибудь пояснить?

Потому что в области памяти могут лежать указатели. И тогда u8 — это байт, который является частью указателя. И тут два пути, либо мы забиваем на то, что u8 — это часть указателя, что стирает инфу о хранимом указателе, либо надо надо каждый байт рассматривать как какое-то значение тип-суммы байта|части указателя. В этом же части статьи чуть ниже описано.

я вот этот момент как раз совершенно не понял. Что значит «стирает инфу». Ну попросим второй, третий байт и по кусочкам соберем информацию о указателе. Если мы просим один байт, то мы практически всегда не получаем полную картину так как подавляющее большинство типов занимает больше 1 байта.

Ну то есть в чем разница, что мы попросили байт от указателя или от u32?

Половина поста посвящена объяснению этого. Указатель 0xFFFE на один массив и указатель 0xFFFE на другой массив — это разные указатели, даже если у них одно и то же байтовое значение.


Если язык будет "стирать" эту разницу (считая, что раз 0xFFFE, то и ладно), то либо будет сильно страдать оптимизация, либо будет много неприятного UB.

Непонятно, почему нельзя просто побайтово копировать структуру, которой представлены указатели. Тогда будет скопирована и информация об области выделения, и о смещении. Зачем все эти сложности с определением байта как части указателя?

А если я скопирую 2 байта из одного указателя, а потом два байта из другого указателя, это какой именно указатель будет?


Это риторический вопрос, на самом деле. Для постороннего человека вся эта трахомудия с указателями ещё менее понятна, чем проблема поиска отображения из одного семигрупоида в другой.


Суть проблемы: если у нас есть указатель на что-то, и компилятор видит, что это указатель единственный, он может оптимизировать код. Например, если из этого указателя читают то, что записали, сравнивают со старым значением и делают if, то весь блок else можно выкинуть, потому что это тафтология.


А теперь кто-то рядом делает указатель на те же данные методом "кастим 8 байт и пишем туда что-то". И это был осмысленный код (с точки зрения того, кто пишет). Что компилятору делать? Три варианта:


  1. Запретить делать фигню с указателями. Мы не можем полностью это запретить, потому что кто-то должен обрабатывать прерывания и переключать real mode в 64-битный режим.
  2. Отключить все оптимизации делать что сказали. Работает (с поправкой на баги человеков), но меееедленно. А все, кто лезут в указатели, хотят быыыыстро.
  3. Объявить это undeined behaviour и делать что делается, и получится что получится. (Текущая ситуация с указателями).

Вот тут вот и пытаются принести здравый смысл в модель указателей без запрета оптимизаций.

Если скопировать 2 байта из одного указателя, 2 из другого, то мы просто получим область памяти, в которой лежат 2 байта из одного указателя, 2 из другого. Как эту область памяти преобразовать в указатель — другая проблема, про преобразование числа в указатель (т.к. любую последовательность бит можно интерпретировать как число). Преобразование числа в указатель в статье не рассматривается, поэтому странно, если целый блок про специальное определение байтов посвящен проблеме, так и не раскрытой в полной мере в статье

Дело не в том, как эту область преобразовать в указатель, а в том, что наличие "неучтённых" указателей ломает оптимизации в других местах (не там, где указатели появляются).


Вся идея поста состоит в том, чтобы добавить к каждому байту (байту!) виртуальной машины низкоуровневого интерпретатора (точнее, эмулятора виртуальной машины) информацию о том, каким именно указателем он является, чтобы сохранить одновременно и здравый смысл и оптимизации.

Затрудняюсь найти источник «неучтенных» указателей, кроме как преобразование числа/области памяти в указатель. И не понимаю, как указанное переосмысление байта помешает нам осуществлять такое преобразование. Если мы получили последовательность байт условно 2 байта одного указателя, 2 другого, то мы на этапе статического анализа запретим преобразование такого блока памяти в указатель?

В статье писали про интерпретатор, обратите внимание. Он такое может.

В языке Rust есть компонент под названием MIRI. Он нужен для интерпретации кода для поиска багов (UB) в рантайме, типа плюсовых санитайзеров, но со специализацией именно в Rust. Работа над ним все еще идет, но он уже нашел несколько багов в стандартной либе (в unsafe части). Ральф как раз и занимается формальной верификацией и MIRI, поэтому периодически выдает всякие статьи о том, как оно внутри устроено, как они хотят что-то улучшить и т.д. Советую почитать его блог.

А если взять скопировать первые 2 байта из float32, а вторые 2 байта из int32? Или даже просто взять 4 числа float32 и взять по байту из каждого из них?
Автор текста вполне донес до меня мысль, что указатель — это вещь в себе, и если они не указывают на одну и ту же область памяти, то с точки зрения компилятора это все равно что разные типы данных.
Вопрос в том, чем указатели отличаются в этом плане от других типов данных? Если взять любой тип данных и начать производить с ним любые побайтовые манипуляции кроме полного копирования, то скорее всего ничего хорошего не выйдет.


UPD. Я еще раз перечитал предыдущее объяснение и понял.
UFO just landed and posted this here

В какой-то момент времени это придётся делать, если спускаться вниз по стеку к более низкоуровневому коду. У нас может прийти сетевой пакет, в котором фрагментация, и граница режет наш любимый int не там, где он заканчивается. Прийдётся делать memcpy, а потом его оттуда вычитывать. Код вычитывания можно сколько угодно обложить красивой типизацией (я не кастю память к типу просто так), но внутри оно всё равно будет кастить память к типу просто так.

UFO just landed and posted this here

Если вы будете приходящие сетевые пакеты вычитывать по-байтно, а объединять сдвигами, то у вас будет непреодолимая проблема — сетевой пакет должен обрабатываться за 8нс (наносекунд) чтобы обеспечить linespeed на 40G. Это 200 пикосекунд на байт, что меньше времени выполнения одной простой инструкции на современных процессорах.

UFO just landed and posted this here

Особенно приятно при оптимизации становится простым тестам памяти и скрабберам.

Я не могу говорить про Си, а в 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 и является одним из лидеров в этой области. Его группа в свое время доказала корректность базовых примитивов стандартной библиотеки в терминах фундаментальных инвариантов языка, для чего они разработали формализм λrust.
Теперь о самом вопросе: C++ ничего не остается делать, кроме как проводить анализ control flow и выяснять, являются ли T* a и T* b указателями на один объект или же на разные.

В 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, при этом результат будет вполне осмысленным.

Вообще не обязательно: 1942 + (2019 - 1942) * 0.5 разницей получили число лет, которое спокойно можно умножать на скаляр (потому как это действительно просто число), а потом к году прибавили число лет — тоже вполне допустимая операция.

Sign up to leave a comment.

Articles