Pull to refresh

Comments 244

А ведь есть языки, лишённые радости указателей!

А может это и не плохо?
Java хороший пример. (Да, знаю что можно использовать ссылки)

Какие такие ссылки?
UFO just landed and posted this here
ни софта на них…
UFO just landed and posted this here
честно говоря не осилил я функциональщину. То ли статей толковых мало, то ли мозгов. Не понял фишки
UFO just landed and posted this here
Ммм… какой map? std::map пользую, а Хаскель причем тут?
UFO just landed and posted this here
UFO just landed and posted this here
Я думаю дело в другом.
Представьте, что вы веган, что бы вы говорили людям?
UFO just landed and posted this here
UFO just landed and posted this here
Ну на самом деле Вы двигаете функциональные языки, словно это какая-то панацея. Все мои потуги что-то написать напоминают мне попытки забить шуруп молотком. У моих объектов в программах есть состояния, которые надо хранить — и то, что я прочел о ф-ных языках напоминает мне костыли. Мои проги пишутся для реального мира — embedded. И куда там это воткнуть — совершенно не понимаю (даже если были бы компиляторы соответсвующие). Простейшая задачка становится математическим ребусом. Вероятно есть, и может даже много, задач, где ф-ное программирование в тему. Я пока что не вижу таких, и судя по минусам Вам — не я один.
Все мои потуги что-то написать напоминают мне попытки забить шуруп молотком. У моих объектов в программах есть состояния, которые надо хранить — и то, что я прочел о ф-ных языках напоминает мне костыли.

Ну так естественно в этом случае у вас будет ощущение забивания шурупом молотка. Функциональщина нужна не за тем, чтоб описывать состояние, а за тем, чтобы состояния не было.


Мои проги пишутся для реального мира — embedded.

Тогда вам точно функциональщина не нужна. Ее смысл в итоге сводится к декларативности (вместо того, чтобы описывать процесс получения результата, описываем сам результат, а уже компуктер догадается, как его получить). В embedded вам нужно описывать процесс by design.

UFO just landed and posted this here
Неа. Если достаточно упороться монадками, то наступает просветление и понимание, что чистоты нет, стейта нет, эффектов нет, потому что это сами по себе не дискриминирующие термины.

Ну так стейта нет — это то, о чем я и говорю. Если же у вас стейт есть, и вы пытаетесь его делать на ФП — то что-то не так. В смысле самого мышления.


Не вижу препятствий в eDSL для этого эмбеддеда (см. вышеупомянутый Ivory), статически тайпчекаемого и верифицируемого, описывающего при этом вполне императивный процесс.

Только DSL и верификация ортогональны функциональному программированию. Вообще, конечно, термин сейчас сильно размыт и следует по-хорошему уточнять, что под фп подразумевается. Я подразумеваю в базе чистую лямбда с надстройками. То есть какой-нибудь Scheme без макросов — сферическое ФП в вакууме.

UFO just landed and posted this here
Вот вы fold или unfoldr делаете, аккумулятор — это стейт?

Стейт — это не то, что в коде написано, а то, как код воспринимается (от кода при этом может зависит насколько сложно описываемые им вещи рассматривать в рамках той или иной парадигмы). Если вы рассуждаете об аккумуляторе фолда как о стейте — это стейт. Если нет — то нет. Аналогично с монадами. Если вы монадический код в IO воспринимаете как просто императивный код — ну, у вас тут стейт.


ФП такого толку — это про типы, ИМХО. Лямбда-исчисление я вам и на плюсах замутить могу.

А можно типы в императивщину добавить. И верифицировать. Или верифицировать без типов. Все-таки тут речь надо вести не просто о типах, а об определенном способе мышления и использования этих типов. В этом контексте правильно говорить не "фп — это когда есть А и Б", а, скорее, "фп — это когда нету А и Б" (и аналогично для любой другой парадигмы). Потому что именно отсутствие альтернатив вынуждает строить программу и рассуждать о ней определенным способом. Наличие же фич — лишь дает возможность.

UFO just landed and posted this here
UFO just landed and posted this here

Одно и то же можно сказать разными способами. То, что можно сказать в ФП, можно сказать и в ООП. И наоборот.

UFO just landed and posted this here
Есть ЯП в которых нет объектов.

Но это не мешает в них объекты использовать, моделируя на имеющихся примитивах. Точно так же как в ООП нет функций, но вы можете их использовать, моделируя при помощи объектов.

UFO just landed and posted this here

Нет, если по парадигмам, то это ближе всего к ФП, наверное. От ООП там вроде и вовсе ничего нету.

UFO just landed and posted this here
Это вы к чему, с-но?
UFO just landed and posted this here
К тому что массивы еще как то в объекты собрать можно, а вот функции нет. На уровне компилятора. И классы кстати тоже

Можно, т.к. язык тьюринг-полный. Только, думаю, кодировка вам откровенно не понравится :)

UFO just landed and posted this here
> Нет нельзя.

Можно.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
там похоже совсем не то. Под эмбеддед я понимаю микроконтроллеры. Вот пишут «описываем сам результат, а уже компуктер догадается, как его получить).» Совершенно не понимаю. Вот сидит себе микроконтроллер. Ждет пакет инфы с радиоэфира. По получении должен сделать то, потом то, все со строгой времянкой и в зависимости от состоянии кучи датчиков. Как тут описать результат — непонятно. Более менее все ясно с числами Фиббоначи только мне. Да и то, там рекурсия (?) за которую в embedded руки отрывают — мало ресурсов. Даже за динамическое выделение памяти не всегда по голове гладят. По-моему язык пригоден для математиков, задачи реального мира если и можно решить, то выглядит это как несчастная сова на глобусе. Но я не сильно вникал в ФП
UFO just landed and posted this here
OK, спасибо за пояснения, думаю мне дергаться на этот счет рано. Вот что реально заставляет нервничать — это С++ со всеми его нововведениями, по-моему они задались целью сделать самый сложный язык на свете. Глядя на некоторые новые исходники на С++ хочется спросить «на каком это языке.» Впрочем отвлекся.
UFO just landed and posted this here
Ждет пакет инфы с радиоэфира. По получении должен сделать то, потом то, все со строгой времянкой и в зависимости от состоянии кучи датчиков. Как тут описать результат — непонятно.

Ну видите, "должен сделать" — это уже не ФП. С точки зрения ФП — будет понятие некоего абстрактного действия. Вы можете создать действие, скомбинировать это действие вместе с какими-то другими действиями каким-то способом и получить новое действие. Пакет инфы и показания датчиков — это данные. Вы описываете, действие какого вида должно получится из каких данных, то есть описываете ф-ю (в математическом смысле) perform: Data -> Action, при этом не специфицируете, как эту ф-ю требуется вычислять. Далее вы можете ввести какие-то проверки на тот факт, что построенный на указанных данных Action удовлетворяет определенным условиям, например, у вас есть ф-я time Action -> int и вам надо убедиться, что time(perform(data)) < n. При этом если вы свой Action составляли из других экшонов при помощи некоторых комбинаторов, то у вас могут быть разные утверждения вроде (time(action1) = a, time(action2) = b => time(sequence(action1, action2)) = a + b); Вот если вы так все будете воспринимать — вы пишете на фп, при этом в качестве языка можете хоть сишку использовать.

боюсь что просмотр итогового кода (в дизасме) может привести к инфаркту, не?
боюсь что просмотр итогового кода (в дизасме) может привести к инфаркту, не?

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

UFO just landed and posted this here
Функциональное Программирование (ФП) можно представить себе как такое написание программы, когда ее выполнение представляет собой как бы вложенные друг за другом «математические функции»: fun1(fun2(fun3(x))) У каждой функции есть совершенно чёткий «вход» и совершенно чёткий «выход», также как у математических (вычислительных) функций: Выход = function(Вход). При этом ни одна функция внутри не обращается к каким-либо переменным/памяти/портам/итд, короче ко всему тому, что представляет собой состояние/память. Подобное состояние, если оно есть (к примеру порт), подается только как «вход». И вдобавок к этому функция должна быть детерминирована (делать одно и то же).
Что это даёт? Подобные функции не зависят ни от чего, кроме как своих входных параметров. И поэтому, если каждый раз перезапускать их с одними и теми же аргументами, мы будем получать на выходе один и тот же результат.
Эмбедщику можно это понять так, что данные словно бы передаются в регистрах, и в регистрах же возвращаются назад, ничего более не меняя.
Это очень удобно и для отладки и для написания стабильных программ — проблемы отладки чаще всего заключаются в том что при работе программы меняется какое-то внутреннее состояние (память, переменные, стек итд) и нет возможности сделать «шаг назад». А при ФП парадигме — достаточно подать на «вход» программы одни и те же предварительно записанные данные, и она выдаст один и тот же результат, пока мы не поправим саму программу.
Парадигма такова, что программа как бы «воздействует» на проходящие через нее данные или их потоки, сама же являя собой «чистый закон».
Что интересно, правильная и рекомендуемая методология *NIX программирования это, в некотором смысле, ФП: мелкие отдельные утилиты, каждая выполняющая свою функцию, принимающие данные на вход, и выдающие детерминированно одно и то же, при том что их можно объединять друг за другом, т.е. выход одной утилиты/команды по-цепочке передавать на вход другой.
Пиксельные шейдеры тоже при определенном приближении являют собой ФП-стиль, вместе с реактивным.
Часть существующих программ и так уже написана приближаясь к функциональному стилю.
Реальное ФП, конечно, подразумевает собой гораздо большее, но кое-какие полезные элементы почерпнуть из ФП, как видим, можно.
Спасибо, достаточно понятно объяснили, хотя реальная применимость мне пока туманна. Вот допустим хотим мы смоделировать покупателя. Чтобы он что-то купил ненужное, он должен продать что то ненужное (например свой труд). Продав, он получит деньги (состояние объекта). Причем поставить покупателя в функцию покупки просто так не очень просто (эти деньги могут лежать в банке, на них идет процент итп). То есть я могу примерно это представить, но совершенно не понимаю, как в ФП реализуется что-то асинхронное, завязанное на время, возникающее от случая к случаю… А еще такой вопрос, глядя на машинный код ФП программы, вы сможете отличить его от кода, полученного компиляцией с императивного языка?
То есть я могу примерно это представить, но совершенно не понимаю, как в ФП реализуется что-то асинхронное, завязанное на время, возникающее от случая к случаю…

В ФП функции — это тоже данные. С-но, действия, процессы, которые при помощи каких-то примитивов комбинируются в их наборы, ивент лупы с хендлерами и любые подобные вещи — это все данные.

Спасибо, достаточно понятно объяснили, хотя реальная применимость мне пока туманна.
Важным преимуществом ФП является параллелизм. Поскольку все функции для вычислений используют только свои параметры, можно организовать вычисление независимых функций в произвольном порядке или параллельно, на результат вычислений это не повлияет. Следование элементам ФП-парадигмы упрощает построение многозадачности (в т.ч. для hardware см. read-only, оно же immutability).
Вот допустим хотим мы смоделировать покупателя.

Это уже дальнейший вопрос, как моделируется предметная область.
1. Функции в ФП устанавливают отношение (relationship). Эмфазис ФП — трансформация значений/данных.
2. Функциональные языки часто гибридные (Object-Functional) — например OCaml, Scala.
совершенно не понимаю, как в ФП реализуется что-то асинхронное, завязанное на время, возникающее от случая к случаю…

«асинхронное, завязанное на время, возникающее от случая к случаю» — назовем это Discrete Event Processsing / Discrete Event Model.
Обработка событий — это область все же ортогональная к парадигме программирования. Это можно понять задавшись вопросом — как это представимо для других парадигм: ООП, процедурной на ЯВУ или таблицы прерываний с обработчиками на ассемблере. Тем не менее, смысл в этом замечании есть, поскольку классически под моделирование систем с дискретными событиями был создан именно ООП (Simula), а вот характерные для ФП relationship,transformation служат для решения задач в другом пространстве. Но так дело выглядит при первом взгляде. Когда же речь заходит о потоках событий, сигналах, и потоковой асинхронной обработке вообще — на сцене быстро появляется ФП. Сугубо практическое применение этому — Matlab и его Simulink.
А еще такой вопрос, глядя на машинный код ФП программы, вы сможете отличить его от кода, полученного компиляцией с императивного языка?
Я это понял как вопрос о парадигме, а не о языках, компиляторах и какие они порождают финальные исполняемые файлы. Императивное и функциональное представление эквивалентны (доказано теоретически). ФП лишь парадигма, также как ООП может быть заменено программированием в процедурном стиле (C++ программа в некоторых компиляторах претерпевает фазу превращения в C. На последнем можно вручную симулировать наследование и виртуальные методы). С другой стороны, при применении почерпнутых техник ФП к обычным программам можно увидеть некоторые архитектурные отличия. К примеру, посреди тела функции не будет обращения или модификации каких-либо переменных. Передача аргументов, возврат значений будет преимущественно by value, при этом можно будет заметить что переданные аргументы (втч данные по указателям/ссылкам) не модифицируются. Будет заметна однократная инициализация без пере-присваивания. Повторюсь, это в самом общем, генеральном смысле, не погружаясь в явные отличия которые порождают компиляторы и их оптимизации.

Как это, а Ptr/ForeignPtr/StablePtr? ;)

UFO just landed and posted this here
UFO just landed and posted this here
В Java то, что мы используем для работы с объектами, вполне себе указатели. Просто мы лишены радости изменять эти указатели или создавать новые на их основе, используя простую арифметику (к примеру, получить таким образом указатель на массив, аналогичный данному, но начинающийся с третьего элемента). Например, если в метод передали (указатель на) объект, то можно спокойно манипулировать содержимым объекта, но вот взять и назначить указателю новый адрес (объекта) так, чтобы это было видно за пределами метода, нельзя. То есть при передаче объекта в метод создаётся копия указателя, с которой метод и работает. Всё это здорово упрощает жизнь, не давая выстрелить себе в ногу на ровном месте. Примитивы при передаче в метод всегда копируются. Если очень хочется менять сам примитив из метода, то его можно обернуть в объект и уже этот объект передать в метод.
Как такие языки решают проблему модификации «тяжёлых» объектов?
Вот надо в картинке изменить пару пикселей — не создавать же полную её копию?
UFO just landed and posted this here
Если вам нужно только изменить пару пикселей и всё, то в любом случае придётся создавать полную копию.

Создание которой можно соптимизировать в мутацию, если доказать, что на объект существует лишь одна ссылка.

UFO just landed and posted this here
В таких языках любой «объект» на самом деле является указателем на соответсвующий объект. Копию нужно всегда делать явно.
Вот иллююстрация на Java:

int i = 1;
int j = 1;
System.out.println(i == j); // true


String i = "1";
String j = "1";
System.out.println(i == j); // false
System.out.println(i.equals(j)); // true
И ни фига подобного.
В зависимости от того, как сложатся звезды у вас первое сравнение в примере со String может выдать true.
А может и false.
ideone.com/BkxRUq
В данном конкретном примере звезды должны бы всегда складываться в true, если пользоваться стандартным javac, а не компилить в байткод самому.
Спасибо за уточнение, попробую перечитать этот момент в спеке.
Спасибо. Прочитал спеку — был неправ.

Дело в том, что компилятор одинаковые строковые константы в пределах класса может записывать в class-файл как одну константу (и делает это в данном частном случае). По ссылке из комментария выше это и происходит.

В Java нет указателей, но при работи с большими массивами int и индексами по ним, задача вполне логически тожественная указателям первого уровня.

Не так страшны указатели, как их арифметика.

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

Правильный подход: богатая система типов, запрещающая творить фигню (Rust!), но сохраняющая указатели в полном объёме.
Вообще говоря — не факт.
Теоретически, языки более высокого уровня могут позволить применить гораздо более эффективные оптимизации, вроде многопоточности или вообще — менять применяемый алгоритм по мере накопления данных прямо во время выполнения.
менять применяемый алгоритм по мере накопления данных прямо во время выполнения

Чем JVM уже некоторое время успешно занимается.
Одно другому не мешает. Однако, значения на стеке всё равно будут быстрее значений на хипе, а каким бы оптимизирующим не был компилятор, его GC всё равно сосёт, потому что есть.
UFO just landed and posted this here
При работе с переменными на стеке нет аллокации и деаллокации. И даже указатель на стеке двигать не надо — lifetimes определяют размер области на стеке на этапе компиляции (я про Rust).

Основная же проблема в том, что GC всё равно должен «узнать» о том, что объект больше не нужен. Если это решение принимается в runtime, да ещё и асинхронно с приложением, то это плохо.
При работе с переменными на стеке нет аллокации и деаллокации.

В GC тоже ее нет. Один раз выделяется nursery (цельный кусок памяти), сохраняется указатель на его начало. Когда рантайму нужна память, то возвращается текущее значение указателя и сам указатель смещается на величину, которая, с-но, требуется для выделения. После исчерпывания nursery указатель просто сбрасывается на начало и процесс повторяется.


И даже указатель на стеке двигать не надо — lifetimes определяют размер области на стеке на этапе компиляции (я про Rust).

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


Основная же проблема в том, что GC всё равно должен «узнать» о том, что объект больше не нужен.

В определенных предположениях можно гарантировать что объекты в nursery недостижимы, с-но ничего узнавать не надо, просто сбрасываем указатель на начало и все (при этом все объекты так и остаются в nursery но они потом перезапишутся). Это не очень хорошо работает в случае, например, ООП, но в случае ФП, когда вам надо выделять огромное количество короткоживущих, "одноразовых" объектов (гигабайты в секунду), эта техника незаменима. Если бы в том же хаскеле было не так а выделялись бы куски по стандартному new то оно бы по сути было неюзабельно.

А каким образом GC знает, где один кусок начинается, а где заканчивается? Где-то же нужно будет записать объект в список, либо использовать какие-то дополнительные пометки, иначе каким образом GC может перенести выживший объект дальше? Когда у вас происходит аллокация на стеке, то смещения на стеке (относительно SP) уже захардкожены компилятором в программу. Нет никакого «списка объектов», каждая переменная — всего лишь разное смещение в памяти (например, SP-0x20). В контексте nursery — это всё равно heap, а heap означает, что программа работает с динамическим указателем.

А каким образом GC знает, где один кусок начинается, а где заканчивается? Где-то же нужно будет записать объект в список, либо использовать какие-то дополнительные пометки, иначе каким образом GC может перенести выживший объект дальше?

Смысл в том, что поскольку у вас практически никогда нет выживших объектов, то и переносить ничего не приходится. При этом определить, что выживших объектов нет, очень просто — поскольку данные иммутабельны, то более молодые данные могут ссылаться на более старые, но более старые не могут ссылаться на более новые. Если в текущем контексте нет ссылок на другие объекты в nurcery, значит, все, что левее — недоступные данные.
В том случае, когда ссылки есть — конечно, приходится проводить скан дальше и перемещать данные в основную память. Но это, еще раз, происходит очень редко.


Когда у вас происходит аллокация на стеке, то смещения на стеке (относительно SP) уже захардкожены компилятором в программу.

В функциональном рантайме все точно так же, у вас в хипе хранятся стекфреймы, относительно начала которых указываются смещение. И вам никто даже не мешает использовать SP в качестве регистра для хранения указателя.


В контексте nursery — это всё равно heap, а heap означает, что программа работает с динамическим указателем.

А чем по-вашему стек отличается от выделенного в хипе куска памяти, указатель на который лежит в SP?

«Почти ничего» не считается. GC должен знать где какой объект хранится. Это означает, что где-то должен быть записан указатель на него.

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

Хранить указатель на heap в SP — это восхитительно. call и ret как делать тогда?

Стек от выделенного куска в heap'е отличается тем, что положение ячейки памяти в heap'е не определено в compile time, а в хороших языках программирования, вроде Rust, смещение относительно SP точно известно. Сам же стек обслуживается процессором, быстрее, чем обычные операции с памятью (аппаратная поддержка).
UFO just landed and posted this here
Угу, нету call'ов в хаскеле. Смешно. Я вбил туда main = putStrLn «Hello, world!» и увидел call'ы. ЧЯДНТ?

Когда указатели в других объектах, их надо писать и читать (указатели) — это обращения к памяти. Чтение по фиксированному смещению от SP всегда быстрее, чем heap->data, потому что само значение heap тоже надо прочитать.
UFO just landed and posted this here
В IO своя атмосфера.

С-но, работы программы на хаскеле заканчивается тогда, когда вернули IO. Что там это IO потом делает — уже не барское дело :)

Чтение по фиксированному смещению от SP всегда быстрее, чем heap->data

А зачем читать heap->data, если можно читать register->data?

Т.е. вы предлагаете для heap'а зарезервировать один регистр общего назначения? На arm вам такое простят, на x86_64 с трудом, на i386 на вас конкретно обидятся.
на i386 на вас конкретно обидятся.

Почему i386, а не БЭСМ-1? Ну и на самом деле всегда можно использовать сам SP.

dpkg-architecture -L|grep -v — -
armhf
armel
mipsn32
mipsn32el
mipsn32r6
mipsn32r6el
mips64
mips64el
mips64r6
mips64r6el
powerpcspe
x32
arm64ilp32
i386
ia64
alpha
amd64
armeb
arm
arm64
avr32
hppa
m32r
m68k
mips
mipsel
mipsr6
mipsr6el
nios2
or1k
powerpc
powerpcel
ppc64
ppc64el
riscv64
s390
s390x
sh3
sh3eb
sh4
sh4eb
sparc
sparc64
tilegx

полный список тут:

gist.github.com/amarao/9856494f69cc28e6280351e3a5fac5ec

БЭСМ-1 не вижу.

А какое отношение данный список имеет к предмету разговора? Вдруг у меня вот свой личный список без i386-х смердящих и с БЭСМами бодрящими!
Он же ничем не хуже вашего списка из ряда архитектур, выбранных случайным образом.

Я привёл список живых архитектур. Список не случайный, а исчерпывающий.
> Я привёл список живых архитектур.

А зачем вы его привели? Вы поясните как-то свой аргумент. Типа: «вот список, по-этому...».
Я его привёл как доказательство, что БЭСМ — мёртвая архитектура. А i386 — живая.
I386, строго говоря, уже мёртвая. Как и AArch32. Хотя AArch32 чуток поживее будет: процессоры без AArch64 ещё выпускаются, а без x86-64 — уже нет.

А так-то компиляторы и для 6502 есть, речь же не о них.
Стек от выделенного куска в heap'е отличается тем, что положение ячейки памяти в heap'е не определено в compile time, а в хороших языках программирования, вроде Rust, смещение относительно SP точно известно.

Положение аргументов относительно стекфрейма тоже известно в компайлтайме.


Сам же стек обслуживается процессором, быстрее, чем обычные операции с памятью (аппаратная поддержка).

Какая аппаратная поддержка? Стек — это обычная область памяти, которую ОС выделяет процессу. Неизвестно положение стекфрейма. Но оно и в расте неизвестно.

Аппаратная поддержка — это SP, call, ret, popa, pusha, pop и push инструкции.
Аппаратная поддержка — это SP, call, ret, popa, pusha, pop и push инструкции.

С-но, если вы не используете call, ret, push, pop, то вам и не нужна эта аппаратная поддержка.

Но без стека вы всё равно не переживёте, потому что первое же прерывание потребует куда-то «всё это» сохранить.
Но без стека вы всё равно не переживёте, потому что первое же прерывание потребует куда-то «всё это» сохранить.

Сохраняйте. В чем проблема? Вам же никто не запрещает стек использовать.

Если вы к тому, что «стек не нужен», можно без него, но в этой ситуации вы не можете использовать SP для другого (прерывания), а его аналог потребует пожертвовать одним из регистров. Можно, но хуже.
а его аналог потребует пожертвовать одним из регистров.

Что, вообще говоря, не проблема.


в этой ситуации вы не можете использовать SP для другого (прерывания), а его аналог потребует пожертвовать одним из регистров.

Почему не могу? ОС же как-то использует и регистрами не жертвует.

К вопросу о необходимости регистров: www.youtube.com/watch?v=lXJ-tYqPARg
и
www.youtube.com/watch?v=nc2q4OOK6K8

Ос не использует никакие регистры общего назначения когда исполняется userspace. Для этого стек (в частности) и нужен — сохранить регистры.
Ос не использует никакие регистры общего назначения когда исполняется userspace. Для этого стек (в частности) и нужен — сохранить регистры.

И какой вывод вы из этого делаете?

Что жертвовать регистром общего назначения ради отказа от использования памяти на стеке — ошибка.
Я не знаю, что и где вы там обнаружили, но тот стек, на который указывает SP в userspace и тот стек, на который указывает SP в ядре — это разные стеки. Да, представьте себе — у процессора много регистров SP. И много стеков. Один регистр SP и один стек — это реальный режим, его перестали использовать до того, как большинство посетителей Хабре родились…
Да, представьте себе — у процессора много регистров SP. И много стеков. Один регистр SP и один стек — это реальный режим, его перестали использовать до того, как большинство посетителей Хабре родились…

Правильно ли я вас понимаю, что вы говорите не о переключаемых контекстах, а прямиком о нескольких hardware регистрах E®SP в x86/64?
Можно ссылку на эту информацию?
Несколько «настоящих» регистров существуют в ARM. В x86 регистр, строго говоря, один (на самом деле нет), но так как при возникновении прерывания он автоматически перезагружается из TSS, то говорить о том, что если мы его задействуем под «кучу», то он станет недоступен для прерываний — несколько глупо.
В x86 регистр, строго говоря, один (на самом деле нет)

Речь шла про x86/64.
Так один регистр стека хардварно, или нет?
Можно ссылку, откуда вы взяли информацию про несколько?
Так один регистр стека хардварно, или нет?
Архитектурно в x86 — он один. На уровне железа — их много. Но это совсем другая история.
Архитектурно в x86 — он один. На уровне железа — их много. Но это совсем другая история.

Как многое можно еще придумать. Вспомнить m68K, CPU SMT/HT, многоядерность, виртуализацию… Смысл вопроса был, конечно, иной. Про количество регистров стека x86/64 с программной точки зрения (для одного, конечно же, ядра):
Да, представьте себе — у процессора много регистров SP. И много стеков. Один регистр SP и один стек — это реальный режим, его перестали использовать до того, как большинство посетителей Хабре родились…

С программной (втч системной) точки зрения, ядро x86/64 имеет единственный регистр стека, перезагружаемый в зависимости от уровня привилегий, событий, и выполнения задач. О случаях © «много регистров SP» всё же не известно…
О случаях © всё же не известно…
Серьёзно?

Смысл вопроса был, конечно, иной. Про количество регистров стека x86/64 с программной точки зрения (для одного, конечно же, ядра):
Да, представьте себе — у процессора много регистров SP. И много стеков. Один регистр SP и один стек — это реальный режим, его перестали использовать до того, как большинство посетителей Хабре родились…
Заметьте, что в той цитате, которую вы столь старательно воспроизвели нет слов x86 — это вы их туда решили добавить. На самой популярной платформа (ARM, если вы вдруг последние 10 лет в коме провели) — регистров SP таки архитектурно не один и не два.

С программной (втч системной) точки зрения, ядро x86/64 имеет единственный регистр стека, перезагружаемый в зависимости от уровня привилегий, событий, и выполнения задач.
В разрезе обсуждаемой проблемы отличие этого варианта от «много регистров SP» (который, как бы вам ни хотелось обратного, тоже существует) — несущественно.
Я не сомневаюсь что в глобальном смысле все ок.
Но иногда интересны только частности. Меня заинтересовало это:
Один регистр SP и один стек — это реальный режим, его перестали использовать до того ...

Т.е. «реальный режим» (real mode), который перестали использовать — это ARM. Если я вдруг последние 10 лет в коме провел, не затруднит ли вас дать мне ссылочку на режим ARM, который бы назывался real mode?

А вот еще интересный вопрос касаемо «перестали использовать». В каком режиме современные CPU x86/64 производства 2018 года стартуют после power-on? (подмигивает)

Если вы про TSS, эта структура находится в памяти.
Плачь шёл про то, что если SP задействовать не под стек, а под что-то ещё, то прерывания невозможно будет обрабатывать.

Но прерывания в защищённой системе ну никак не могут обрабатываться на пользовательском стеке — иначе вся защита насмарку.

Загружается ли «системный» стек из памяти (x86, TSS) или из «теневого регистра» (ARM)… не принципиально.
Что жертвовать регистром общего назначения ради отказа от использования памяти на стеке — ошибка.

Поясните, почему?
С-но, выше вам уже ответили про то, что в реальности, на современных архитектурах и в современных ОС в один момент времени существует много разных стеков. И ничего не ломается.

На дворе 2018й, а вы про реальный режим и MS DOS. Зачем заниматься некрофилией?
> Но без стека вы всё равно не переживёте, потому что первое же прерывание потребует куда-то «всё это» сохранить.

Вот пример, как это совсем без аппаратно определённого стека делается на S/360 и её потомках вплоть до z/Arch:

1. При приходе прерывания процессор сохраняет PSW (это, грубо говоря, IP + Flags) по фиксированному адресу, назовём X1 (зависящему от типа прерывания) и читает новое значение из другого фиксированного адреса (X2) для этого прерывания.

2. Код по новому адресу в первую очередь сохраняет один регистр, например, R1, по фиксированному адресу X3 в памяти (позволяется абсолютная адресация от 0 до 4095 до S/390, и в пределах мегабайта — после), читает из X4 новое значение для R1 (пусть это X5), а затем одной командой STM сохраняет все остальные регистры по этому адресу (15 общих регистров => заняты адреса от X5 до X5+119).

3. Теперь он свободен в использовании всех общих регистров, кроме R1. Юзерское значение R1 читается из X3 в любой другой регистр и пишется в область сохранения (например, по X5+120). Значение в X3 больше не нужно: это была одноразовая временная ячейка.

Теперь по необходимости точно так же сохраняются регистры плавающей точки.

4. Инициализируется обстановка для работы обработчика прерывания: при этом может создаваться и стек, если надо (много софта работает в таком режиме). В userland обычно стек адресуется через R13, операции доступа к нему просто пользуются доступом в формате регистр+смещение.

5. Если требуется реентерабельность для прерывания, то выполняются дополнительные действия:
1) сохранённый адрес возврата (в X1) кладётся на стек (или в аналог — это может быть область из динамической кучи);
2) выделяется новая область сохранения и её адрес (новый X5) кладётся по адресу X4; точно так же, это выделение может быть и на стеке, и где-то в другом месте;
3) разрешается прерывание в PSW (до этого оно должно было быть запрещено).

Все описанные операции банальные и укладываются в десяток команд. При выходе из прерывания делается то же самое в обратном порядке. Финальной операцией является LPSWE — аналог iret, но с указанием, по какому адресу находится PSW для восстановления.
В ARM, например, регистр стека ничем не выделяется в этом смысле от других регистров, относительно которых можно адресоваться. (В AArch64 есть тонкости, что есть регистр 31 в данной команде, но я не об этом.) И есть много других подобных архитектур. Даже в PDP-11 особые свойства SP существенны только на переключениях уровней привилегий.
Call и ret на многих RISC и не только (например, на z/Arch) делаются не через стек, а через регистр связи.
И на x86 есть аргументы за использование смещения относительно esp/rsp вместо push/pop — так полностью делает текущий компилятор Go (в его выхлопе вы не найдёте ни одной push или pop); так делает GCC, если у целевого процессора плохо с оптимизацией последовательностей push/pop, как было в P4.
Про pusha/popa лучше бы вообще не вспоминать, их сейчас не используют даже в ядре.
Про pusha/popa лучше бы вообще не вспоминать, их сейчас не используют даже в ядре.
Как можно использовать команды, которых не существует??? Их выпилили при переходе на x86-64 (поддерживаются только в 32-битном режиме для совместимости и работают весьма медленно на современных CPU).
Вам надо двигать указатель на сам стек, что эквивалентно движению указателя в nurcery.
Раз уж придираемся, то в случае стека мы двигаем значение в регистре, а в случае GC мы пишем в память, и скорее всего interlocked'ом — разница очень велика.
При этом сброс nurcery происходит быстрее, чем сброс стека.
С чего бы это вдруг? Копированием релевантных данных можно пренебречь?
Если бы в том же хаскеле было не так а выделялись бы куски по стандартному new то оно бы по сути было неюзабельно.
А кто заставляет использовать самый примитивный способ менеджмента памяти?
в случае ФП, когда вам надо выделять огромное количество короткоживущих, «одноразовых» объектов (гигабайты в секунду), эта техника незаменима.
Не эксперт в хаскеле и ФП, но насколько я знаю, функциональщина всегда stateless. Так зачем тогда в принципе что-либо кроме стековых аллокаторов?
Раз уж придираемся, то в случае стека мы двигаем значение в регистре, а в случае GC мы пишем в память

Вообще-то стек — это часть памяти. Когда вы пишите в стек, вы тоже пишите в память. В ту же самую память. Другой нет.


С чего бы это вдруг? Копированием релевантных данных можно пренебречь?

Релевантных данных практически никогда нет, с-но нечего копировать.


А кто заставляет использовать самый примитивный способ менеджмента памяти?

Ну, чтобы способ был не примитивный — вам надо реализовать алгоритм gc.


Не эксперт в хаскеле и ФП, но насколько я знаю, функциональщина всегда stateless. Так зачем тогда в принципе что-либо кроме стековых аллокаторов?

Ни зачем, мы же как раз и обсуждаем тот факт, что аллокация в nurcery и в стеке — это практически одно и то же.

Вообще-то стек — это часть памяти. Когда вы пишите в стек, вы тоже пишите в память. В ту же самую память. Другой нет.
Я говорил про аллокацию/деаллокацию, а не про само использование. И кстати, это не точно та же память, а наиболее интенсивно используемая память, которая следовательно практически гарантировано будет в кэше.
Релевантных данных практически никогда нет, с-но нечего копировать.
А проверкой на то что их нет можно пренебречь? А если их нет, чем циклический аллокатор не подходит?
Ну, чтобы способ был не примитивный — вам надо реализовать алгоритм gc.
Ну GC — это как бэ не единственная альтернатива new/delete.
Ни зачем, мы же как раз и обсуждаем тот факт, что аллокация в nurcery и в стеке — это практически одно и то же.
Зачем тогда делать функциональную VM с использованием GC?
UFO just landed and posted this here
В каком смысле статически? Если в смысле того, что не понятно заранее, сколько выделять, то есть alloca. Если распределять между потоками, то кастомный стековый аллокатор и танцы с виртуальной памятью.
UFO just landed and posted this here
И кстати, это не точно та же память, а наиболее интенсивно используемая память, которая следовательно практически гарантировано будет в кэше.

Ну так и nursery будет в кеше.


Я говорил про аллокацию/деаллокацию, а не про само использование.

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

Я говорю об аллокации/деалокации как о логической операции, а не как о вызове malloc/free или аналогичных API. При аллокации на стандартном стеке потока максимум, что может произойти — операция добавления к регистру. В описанной Вами модели при аллокации нужно как минимум модифицировать значение в памяти, а это намного дольше модификации регистра даже если каким то образом оно окажется в кэше. Уже на этом этапе GC проигрывает, а впереди еще собственно сборка мусора, которой на стеке нет вообще.
UFO just landed and posted this here
В описанной Вами модели при аллокации нужно как минимум модифицировать значение в памяти

Зачем? Точно так же инкрементируете регистр.


GC проигрывает, а впереди еще собственно сборка мусора, которой на стеке нет вообще.

Сборка мусора из nurcery — это просто изменение значения в регистре с текущего на начало nurcery.

UFO just landed and posted this here
> У каждого ядра свой кусок nursing area, не нужно думать о многопоточности.

У ядра или треда?
В первом случае, как решается вопрос при миграции треда на другое ядро?
В GC тоже ее нет. Один раз выделяется nursery (цельный кусок памяти), сохраняется указатель на его начало.

Вам надо двигать указатель на сам стек, что эквивалентно движению указателя в nursery.

вам уже указали разницу между увеличением регистра и указателем на блок, отмечу также, что там как минимум должна быть проверка на выход за границы nursery.
но в случае ФП, когда вам надо выделять огромное количество короткоживущих, «одноразовых» объектов (гигабайты в секунду), эта техника незаменима.

проблема не в том, чтобы много памяти выделять/освобождать, а в том, чтобы эту память инициализировать. Для ФП, завязанном на иммутабельности, данные либо постоянно копируются («гигабайты в секунду» эффективно не покопируешь), либо используются специальные COW структуры данных, с собственными накладными расходами. Компилируемым императивным языкам иммутабельность не нужна, и там можно вручную управлять памятью куда эффективнее, даже если она будет выделена через стандартный аллокатор
вам уже указали разницу между увеличением регистра и указателем на блок, отмечу также

Нет, разницы нет.


там как минимум должна быть проверка на выход за границы nursery.

А в случае стека должна быть проверка на пробой стека.


проблема не в том, чтобы много памяти выделять/освобождать, а в том, чтобы эту память инициализировать.

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


Компилируемым императивным языкам иммутабельность не нужна, и там можно вручную управлять памятью куда эффективнее, даже если она будет выделена через стандартный аллокатор

Управлять — да, а выделять и освобождать — нет.

Нет, разницы нет.
Разницы нет, пока вы не понимаете устройство современных процессоров.
А в случае стека должна быть проверка на пробой стека.
В стандартном стеке да, но кастомный стековый аллокатор можно сделать «бесконечным» с помощью виртуальной памяти.
Разницы нет, пока вы не понимаете устройство современных процессоров.

Ну расскажите мне про устройство современным процессоров в данном контексте.


В стандартном стеке да, но кастомный стековый аллокатор можно сделать «бесконечным» с помощью виртуальной памяти.

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

Ну расскажите мне про устройство современным процессоров в данном контексте.
Разница в том, модифицировать ли регистр, или память. Даже закэшированное значение модифицируется не бесплатно.
Тогда соответствующая проверка будет в логике этого аллокатора. Вы же не можете довыделить память под стек, не убедившись, что имеющаяся закончилась?
Вообще то могу) Просто проверки будут делаться на уровне ОС. Это делается правильным менеджментом виртуальной памяти. Правда, ОСшное выделение будет производиться по 4KB, что маловато, так что не факт, что такая экономия одного if'a того стоит.

Вообще, лично меня смущает то, что я так и не понял, чем именно простые стековые аллокаторы не подходят в функциональщине. Ведь по сути и речи не идет о том, что GC может быть лучше стека, лишь отстаивается то, что он может работать не сильно медленнее. Так почему бы не использовать стек, который заведомо не менее производительный, и гораздо более тривиальный в реализации?
UFO just landed and posted this here
Разница в том, модифицировать ли регистр, или память.

В обоих случаях можно модифицировать регистр.


Вообще то могу) Просто проверки будут делаться на уровне ОС.

Ну, то есть не можете, т.к. ОС все равно их сделает :)


Вообще, лично меня смущает то, что я так и не понял, чем именно простые стековые аллокаторы не подходят в функциональщине.

nurcery — это и есть стек в фп. Но он не может быть смоделирован классическим стеком, т.к.:


  1. из-за замыканий стек становится деревом фреймов, а не их списком
  2. из-за лени он становится произвольным графом

в итоге вы не сможете освободить память прямым раскручиванием стека, если там окажутся достижимые данные.

Нет, разницы нет.

почитайте про инвалидацию кеша процессора
А в случае стека должна быть проверка на пробой стека.

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

Управлять — да, а выделять и освобождать — нет.

Хорошо, с другой стороны. Прикрутив джавовский или любой другой аллокатор к приложению на c++/rust, мы получаем ту же скорость выделения/освобождения (с теми же накладными по памяти), но меньше копируем данные
почитайте про инвалидацию кеша процессора

С точки зрения кеша в nursery все точно так же как со стеком.


делается на уровне железа.

Нет, не делается. Этим занимается ОС, т.к. именно ОС выделяет процессам стек.


Хорошо, с другой стороны. Прикрутив джавовский или любой другой аллокатор к приложению на c++/rust, мы получаем ту же скорость выделения/освобождения (с теми же накладными по памяти)

Ну, я, вроде, об этом и сказал — вы можете переизобрести (или использовать уже написанный) gc.

С точки зрения кеша в nursery все точно так же как со стеком.

Нет, не делается. Этим занимается ОС, т.к. именно ОС выделяет процессам стек.

Я про аппаратную поддержку. amarao вам уже про это писал
Ну, я, вроде, об этом и сказал — вы можете переизобрести (или использовать уже написанный) gc.

не путайте автоматическую сборку мусора с деаллокацией
Я про аппаратную поддержку. amarao вам уже про это писал

Ну аппаратная поддержка — это использование конструкций вроде pop/call/etc. Если вы их не используете, то и проблем от отсутствия аппаратной поддержки у вас нет.


не путайте автоматическую сборку мусора с деаллокацией

Я и не путаю.

UFO just landed and posted this here
UFO just landed and posted this here
Иммутабельность очень сильно помогает GC.

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

А если разобрать несколько иначе (людоедо_едо_ед = тот, кто ест еду людоеда = собственно, людоед), то его обед это человек, а еда обеда — обычная человеческая еда. Которую ученая, изучающая людей, пригласила с очевидной целью — съесть. )
В таком случае вы промежуточно имеете разбиение людоедо_едоед, а слово «едоед» в русском языке отсутствует.
Формально да. При первом прочтении стишка у меня оно в голове само построилось по аналогии с «мясоед» и подобными.
Вполне может быть, что людоед — лев, людоедоед — блоха, людоедоедоед — лягушка.
Тогда людоедоедоед != людоеду.
Ну а энтомолог Иванова пригласила льва, чтобы посчитать блох ;)
Блоха львов всё же не ест. Так, надкусывает.
«Любую проблему можно решить путём введения дополнительного уровня абстракции, кроме проблемы слишком большого количества уровней абстракции»
людоедоедоеда», нам нужно произвести лингвистический анализ последнего слова. Проще всего это сделать, читая корни справа налево:

Людоед = тот, кто ест людей.
Людоедоед = тот, кто ест людоедов = тот, кто ест тех, кто ест людей.

Простите, но сразу стало непонятно. Самый правый корень "ед". Может слева направо?

Как раз справа налево. Правый корень «ед» заменяем на фразу «тот, кто ест», словарная основа остаётся. Также как «T*» читается как «указатель на T», то есть, справа налево.

Вы сейчас пытаетесь объяснить рекурсивно, через указатели :). Я же говорю о логике объяснения в статье.
Если мы разбиваем слово людоедоедоед, тогда первая строчка (по Вашему описанию) должна быть


  1. людоедоедоед = людоедоедо (лево) + ед (право)
    То есть у вас словесный алгоритм не соответствует написанному коду. А для вот таких вот объяснений-аналогий это как раз самое важное.
Вы имеете в виду, что строки в другом порядке должны быть? Так эти строки — не пошаговое выполнение метода, а просто иллюстрация его на всё более сложных примерах.
Забудьте. Гораздо проще разобрать эту загадку, используя указатели из C, чем наоборот.

А что, обьяснения, что указатель — это адрес в памяти, где лежит объект не достаточно? Подход в статье ну совсем какой-то мутный.

Кому-то достаточно. Лингвистическая аналогия — исключительно для тех, кто не понял с первого раза.
подавляющее большинство с первого раза не поймет ни сухое объяснение через адреса/номера ячеек, ни тем более вашу эм… аналогию. ~90% — визуалы, и объяснять в большинстве случаев надо через рисунок; остальные же редко идут в точные науки, требующие пространственного мышления.

А если непонимающему просто дать задачу, где указатели реально нужны и он сам придет к их необходимости.
Мне кажется, нет смысла объяснять указатели дальше одного уровня.
Вещи типа TObject* objects[] должны как-то сами приходить.

Я и говорю: нужен щелчок.
Нет конечно, отсюда и все эти бесконечные статьи. Указатель — это переменная, содержащая адрес, а не сам адрес. Упрощая, вы вводите новичка в заблуждение. Отсюда всякие непонятки с арифметикой над указателями и пр. ((int*)55555+1 = 55563 на 64-х битных платформах). Лучше сказать так: указатель — это некая абстракция над местоположением некоторой сущности в памяти (зависит от реализации, это может быть вообще какой-то дескриптор на экзотических архитектурах, а на интелах — это смещение внутри сегмента, даже здесь не назовешь это просто адресом).
На самом деле как ни говори, а проблема в том, что указатель — это вещь, которая объединяет несколько разных сущностей, ни об одной из которых новочёк понятия не имеет. Вот все эти «адреса», «память» и прочее — для него «тёмный лес и куча дров».

Лучшее, что я видел — это «отодвинуть» изучение указателей за ту точку, когда человек уже знает для чего они нужны.

То есть вначале мы изучаем массивы, структуры, дальше изучаем алгоритмы (деревья, очереди с приоритетами). А потом, когда человек уже привык оперировать с индексами, которые указывают на ячейки, в которых тоже лежат индексы — объяснить ему, что указатель — это такой «синтаксический сахар» для большого массива с названием «опереативная память» — дело пяти минут.

И всё. И нет никакой магии и никакого «щелчка» в голове, всё буднично и банально.
подскажите, как сделать дерево без указателей?
Пр изучении программирования деревья (да и списки, стеки, деки, очереди) обычно вводят на базе массива. Заодно показывается принцип сборки мусора.
Это не «обычно». Это «как надо». Потому что «обычно» стремяться перескочить через все «скучные» вещи и научить какие-нибудь картинки рисовать. Или web-страничку генерировать.

Почему-то никого не удручает тот факт, что для того, чтобы научиться рассказы писать нужно потратить не один год, рисуя палочки и изучая как подлежащее со сказуемым согласуется.

А потратить пару месяцев на то, чтобы понять как устроены базовые, простейшие структуры данных (и на их основе понять как работают указатели и рекурсия) — нам влом.
Да даже через русский язык стремятся перескочить, благо там «собирается» даже с ошибками.
Бинарное дерево без указателей, на массиве длиной в 2^n, описано у Кнута, ЕМНИП. Там примерно так: в массиве лежит по индексу 1 корневой элемент, потом оба его дочерние, за ними — 4 их дочерних, и т.д. Нулевой и отсутствующие элементы помечаются как пустые. Для элемента по индексу idx: переход к родителю — idx/2, к левому/правому потомку — 2*idx и 2*idx+1. В каких-то (редких?) случаях так даже может быть экономнее и быстрее, чем на указателях. Disclaimer: я сам всегда на указателях деревья строю, но из песни слова не выкинешь)
То, что вы описали — это то, как реально устроена куча. Тоже интересный вариант, но для обычного дерева — очень плохой, если у вас там дерево не сбалансировано потеряете кучу места. Для реализации очередей с приоритетами — то, что доктор прописал.
Вот поэтому я и написал, что в редких случаях. Если дерево заполняется так, что перебалансировка не происходит, оно (почти) полностью заполнено, и используется только для поиска элементов, то поиск может быть довольно быстрым, за счет высокой локальности — промахов процессорного кэша будет поменьше, чем для разбросанного в памяти дерева на указателях.
за счет высокой локальности — промахов процессорного кэша будет поменьше, чем для разбросанного в памяти дерева на указателях.
Вот только никакой локальности в этом представлении нет и в помине, так что реально выигрыш происходит только за счёт экономии памяти.
При обработке деревьев, как правило, нужно переходить к левому и правому потомкам, иногда наверх. И крайне редко — к «соседу» слева или справа. А при хранении дерева как вы описали — близко расположены именно «соседи», а полезные ячейки (все три) — очень и очень далеко. На таком дереве разве что «поиск в ширину» хорошо организовывается.
Я писал про поиск — там только сверху вниз, и чем ближе элемент к корню, тем чаще по нему бегается; но чем ближе к корню, тем компактнее они размещены. Соответственно, в кэш попадает меньше мусора, и в результате он будет использоваться лучше, чем при размазанной структуре. Но если дерево сильно больше размеров кэша, эффект будет невелик, да.
Это интересное замечание, но, боюсь, неиспользуемые ноды засорят кеш. Хотя, да, было бы неплохо провести исследование. C AVL деревьями даже, может, какой-то выигрыш будет. С красно-чёрными — вряд ли…

Если надо, то можно сделать под дерево кастомный аллокатор и обеспечить себе любую требуемую локальность, вообще говоря. Так что особо не важно что там и как сорит.

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

Не обязательно большим куском, можно кусками поменьше. Сборщики мусора же как-то работают.

Сборщики мусора работают за счёт дополнительной информации. А если вы вы всё-таки заглянули в начало дискуссии, вместо того, чтобы бредить, то обнаружили бы, что обсуждается хранение дерева, когда ссылки left и right отсутствуют, являются виртуальными…

Вы уже сами запутались. Давайте по порядку:


  1. Если дерево сбалансировано, то вы можете выделить элементы любом наперед заданном порядке и расположить их последовательно, обеспечив локальность для любого наперед заданного метода обхода. Не важно со ссылками или без.
  2. Если дерево несбалансировано, то вы так же можете выделить ноды в любом наперед заданном порядке и расположить их последовательно, обеспечивая локальность, но уже используя указатели, чтобы компенсировать "дырки". Толку от неиспользования указателей в этом случае нет, т.к вам "дырки" сожрут всю сэкономиленную память.
Собственно вы сейчас повторили ровно то, что я написал. И далее зачем-то предложили в конкретную версию варианта 1 (которому вся эта ветка посвящена) вкручивать аллокатор. Куда его там вкручивать и чем он там поможет — мне лично неясно.
Очень просто. Заводите массив размером с максимальное количество элементов в этом дереве. Вместо указателей — индексы.

На самом деле проще всего начать с такой структуры данных, как «ассоциативный массив» на базе хеш-функции.

Вначале — простой массив пар «ключ/значение», в случае если случаются коллизии… у нас беда.

Потом — начинаем записывать в соседние ячейки… и на какое-то время можно на этом продержаться.

Потом — учимся обходить занятые места и устраивать «дыры», если нам это нужно (тут у нас уже получаются вначале односвязные, а потом и двусвязные списки).

Ну а после этого — можно уже и деревья устроить.
Вы уверены, что 55555 и 55563 — допустимые значения для адреса целого в ILP64 системах?! ;)
На большинстве современных процессоров — да. Но на Alpha, почившей в бозе — нет.
ARM, POWER — в том же месте, что и x86: теоретически можно самому себе строить проблемы и запретить доступ по невыровненным адресам, но пракстически — этот режим никто не использует. Про SPARC не знаю.
Хммм, вы меня немного запутали. А кто эти люди кому пытаются обьяснить указатели и они не понимают? Они вообще понимают, как работает процессор?
Получается, что есть какой-то абстрактый человек, который понимает сегменты и смещения в них процессоров Intel, он знает про разницу 32-х и 64-х битной адресации, а когда ему сказали, что '&' берет адрес переменной, а '*' возвращает значение по адресу, он такой:«Нет, вы знаете, не догнал». Странно это как-то.
Интересно, а эти самые люди ссылки в С++ понимают?
Ссылки это вообще абстракция над абстракцией, их никто не понимает ). А вообще я не согласен лишь с тем, что сишный указатель — это адрес в памяти. Это довольно избитая и обмусоленная тема, вот тут почитайте.
Тут я с вами согласен. Адрес в памяти конечно не точное определение, но покрывающее большинство распространенных архетиктур. Ваше определение формально более точное, но воспринимается тяжелее.
Похожая история и с байтом происходит. Его упрощают до 8-ми бит т.к. количество архитектур где он другого размера крайне мало.
Это довольно избитая и обмусоленная тема, вот тут почитайте.
А что я там должен увидеть? Что люди, не знающие о том, как устроен современный C пишут о нём всякие домыслы? Беглый поиск показал, что никто из спорщих о вкусе устриц с теми, кто их ел, не подозревает о том, что с C99 (а стало быть и с C11) есть такая штука, как intptr_t, которая «закрывает тему»: да, теоритечески указатели в C (и C++) могут быть много чем (intptr_t, строго говоря, опционален), но практически — вы можете просто игнорировать подобные компиляторы.
Осталось определиться, какую тему закрывает intptr_t, и отсылок к стандарту в той теме как раз предостаточно. Предмет текущего спора, напомню, можно ли утверждать, что указатель (pointer в оригинальном стандарте) — это адрес. А не какие типы данных появились в каком стандарте.
Если как раз почитать стандарты, на которые вы ссылаетесь, то однозначный ответ — нет. Да, численно указатель равен смещению в сегменте данных, но на этом все сходство с offset ptr заканчивается (кстати, даже в оригинальных ассемблерах не употребляют слово адрес, а именно offset — смещение).

В высокоуровневых языках, где вообще нет указателей, не нужно забивать голову низкоуровневой ерундой, но в С извольте называть вещи своими именами и понимать из каких частей и как именно формируется адрес в памяти.
Осталось определиться, какую тему закрывает intptr_t, и отсылок к стандарту в той теме как раз предостаточно.
Вопрос «является ли указатель числом».

The following type designates a signed integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:
    intptr_t
The following type designates an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:
    intptr_t
Если мы можем сконевртировать указатель в число, проделать с этим числом всем операции, какие можно производить с числом (в файл, например, записать, или по сетке на другой компьютер послать), то всё, тема закрыта: этот указатель — таки и есть число.

Предмет текущего спора, напомню, можно ли утверждать, что указатель (pointer в оригинальном стандарте) — это адрес.
А что такое адрес, извините?

Да, численно указатель равен смещению в сегменте данных, но на этом все сходство с offset ptr заканчивается (кстати, даже в оригинальных ассемблерах не употребляют слово адрес, а именно offset — смещение).
Если вы таки про 8086, то там ещё и сегмент был. И указателей было… много разных: near, far, hure

В высокоуровневых языках, где вообще нет указателей, не нужно забивать голову низкоуровневой ерундой, но в С извольте называть вещи своими именами и понимать из каких частей и как именно формируется адрес в памяти.
Это, в общем-то, нужно только при взаимодействии с ассемблером. Из-за вышеописанного свойства intptr_t указатель — это число. А уж как они связано с тем, что на шину адреса выставиться — дело десятое. Может со сдвигом, может инверсно, может ещё как-нибудь. Но так или иначе — это должно проивходить как-то, иначе указатель свою роль не сможет играть.

P.S. Собственно на это наткнулись эльбрусовцы, когда завели теги в памяти, чтобы указатели перестали быть просто числами и стали чем-то что OS выделяет и на корректность чего, соотвественно, можно положиться. Вот в тот момент, когда выяснилось, что указатель можно превратить в число и потом — обратно в указатель… вся схема и «провисла:»…
Ну про число никто не спорит, там вообще все имеет численное представление, даже буквы.
Буквы как раз во всех известных мне языках можно в число и обратно преобразовать. А вот указатели — не во всех. В ISO Pascal или в Ada — нельзя. А потому что «безопасность», а потому что iAPX 432, а потому что указатель — это не просто адрес.

А вот в C (по крайней мере в современном C) — это таки «просто число», то есть «просто адрес».
Может быть, в C и число, и указатель — это просто битовые паттерны с разными правилами интерпретации компилятором: старший бит — знак и т.д. Или 4 бита — номер банка и т.д. Нет?
Дык об этом и речь! В C (по крайней мере при наличии в нём intptr_t) указатель — это просто набор бит, битовый паттерн. Его можно превратить в число и куда-нибудь «послать»… а ведь не на всех машинах это возможно. Забудьте про интерпретаторы, почивший в бозе iAPX32 и даже Эльбрус (хоть вроде как последний таки жив).

Вспомните про .NET! Там указатель — это не просто себе последовательность битов. За ним учёт нужен. Превратить его в число, а потом обратно — низзя. Не положено. А в C/C++ — как раз положено! И всё — в .NET встроить C/C++ нельзя. Недаром Micrososft спецподелие изобрёл. Не потому, что ему больше нечем заняться…
Это люди, которым в учебнике написали: вот так объявляется массив объектов, вот так — массив указателей на объекты. Вот такой синтаксис обращается к методу объекта по его указателю. Вот так работает оператор new []. Теперь напишите программу, которая будет производить масштабирование сложной геометрической фигуры на плоскости.

В итоге человек, который не до конца понимает, что делает синтаксис, который он использует, начинает писать код, который выглядит как пример, который я дал в самом начале. Подобную ситуацию приходилось видеть не раз и не два, даже если человек честно читал учебник — увы, не все они написаны последовательно, а багаж знаний, накопленных в таких языках, как JavaScript, ещё более усложняет понимание.
Получается, что есть какой-то абстрактый человек, который понимает сегменты и смещения в них процессоров Intel, он знает про разницу 32-х и 64-х битной адресации, а когда ему сказали, что '&' берет адрес переменной, а '*' возвращает значение по адресу, он такой:«Нет, вы знаете, не догнал».
Нет такого астрактного человека. Есть человек, которому написали как получить данные из формочки и «нарисовать» на основе этих данных HTML.

Всё равно как если бы математику учили не со сложения и таблицы умножения, а с задач по составлению диффуров и нахождения экстремумов. Понятно, что это полезнее, но без основ — оно всё у вас «провиснет в воздухе» и вы будете делать совершенно идиотские ошибки на ровном месте.
А чего же он эти данные с формочки парсит на С/C++ не понимая указатели а не на каком-нибудь Python или, прости господи, PHP?
Проблема не в том, что он парсит формочки на C/C++. Проблема в том, что он считает себя программистом, которому чуть-то нужно подучить C/C++ и всё. Хотя реально — он занимается комбинаторным программированием, дёргая примеры со StackOverflow и комбинирует их случайным образом пока результат не будет походить на то, что требуется.

А потом вот с этим багажом — хотят изучить C/C++. Причём в том же стиле. А он, зараза, понимания требует. Комбинаторным программированием в нём даже программу в 1000 строк не сделать. Вот беда-огорчение…

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

Да ладно вам, перекладывание байтиков по когнитивной нагрузке от дерганья формочек ничем существенно не отличается.
Отличиается. Тем, что если вы «не так» переложите формочки — то вы это сразу увидите или, в худшем случае, вам прилетит понятный exception.

Если вы «не так» переложите байтики — вам никто худого слова не скажет, просто программа будет вываливаться с невразумительными дампами (да, я знаю, есть много способов во всём этот разобраться — но для этого всё равно нужно какое-то понимание того, что вы делаете).
Отличиается. Тем, что если вы «не так» переложите формочки — то вы это сразу увидите или, в худшем случае, вам прилетит понятный exception.

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

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

Ну, во-первых, можете увидеть и спустя несколько лет после эксплуатации, если проблема в логике.
Те, кто могут писать нетривиальную логику — уже и в указателях могут разобраться.
Достаточно понять как меняется программа «методом малых шевелений».

exception от этого понятным не станет


Те, кто могут писать нетривиальную логику — уже и в указателях могут разобраться.

Так я об этом и сказал с самого начала. Перекладывание формочек от перекладывания байтиков ничем не отличается. И там и там — перекладывание. Сложность возникает тогда, когда процесс перекладывания становится нетривиальным, и не важно, формочки это или байтики. Если же процесс тривиален — то, опять же, разницы нет, формочки у вас или байтики, в обоих случаях можно будет справиться "малыми шевелениями" и прочим стековерфлоу-программированием.


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

Достаточно понять как меняется программа «методом малых шевелений».
exception от этого понятным не станет
И? Задачу после некоторого количества случаных измнений вы решите, эксепшн изгоните — значит вы уже программист. Всё как у классика.

А вы сюда с каким-то «пониманием» лезете.

Сложность возникает тогда, когда процесс перекладывания становится нетривиальным, и не важно, формочки это или байтики.
Важно. Формочка с тремя полями и база с двумя стобцами — это уже что-то за что можно какую-то денежку, если повезёт получить. А три байта в C++ — этого даже чтобы светодиодом поморгать, скорее всего, не хватит.

Количество переходит в качество и комбинаторное программирование перестаёт работать…
Важно. Формочка с тремя полями и база с двумя стобцами — это уже что-то за что можно какую-то денежку, если повезёт получить. А три байта в C++ — этого даже чтобы светодиодом поморгать, скорее всего, не хватит.

Мне кажется, то, что в байтоперекладывании за "сложность" платят меньше — факт ортогональный обсуждаемым вопросам.

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

Она просто развалится под собственной тяжестью. И этот размер — особо от языка не зависит.

Но в случае с web-программированием программы подобного размера можно продавать, а в случае с C++ — нет.

А это кардинально меняет всю динамику.

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

Подход в статье ну совсем какой-то мутный.

Мне эти «людоедоедоеды» вообще напомнили цитатку с башорга:
Заголовок спойлера
«Те, кто водит хороводы — хороводоводы. Те, кто изучают творчество хороводоводов — хороводоводоведы. Те, кто любит читать хороводоводоведов, — хороводоводоведофилы. Те, кто ненавидит хороводоводоведофилов, — хороводоводоведофилофобы. Те, кто поедает хороводоводоведофилофобов, — хороводоводоведофилофобофаги. Те, кто ведет борьбу с хороводоводоведофилофобофагами, — антихороводоводоведофилофобофаги. Те, кто выдает себя за антихороводоводоведофилофобофагов, — квазиантихороводоводоведофилофобофаги!!!»

Извините, не удержался)
У части студентов понимание этой достаточно простой штуки «щелкает» сразу, а часть неделями не может разобраться и постоянно путается из-за этого. Способ приведенный в статье, конечно, очень странный, но проблема, которую статья ставит — существует.
void do_something(MyObj *input[], int count)
{
    MyObj **copy = new MyObj*[count];
    for (int i = 0; i < count; ++i)
        *copy[i] = *input[i];
    ...
}

В коде ошибка. copy — это массив указателей.
В выражении:
*copy[i] = *input[i];

происходит разъименование указателя copy[i], который никуда не указывает. Это UB.
Это все, конечно, занятно понимать just for fun, но на С++ так писать не надо (в принципе, в 99 процентах случаев больше одного указателя не надо).
потому что вы на GPU не программировали — я там это добра даже поболе.
Еще с потоками, с другим принципом обхода массивив и т.д.
CUDA C — не С++. В статье речь идет о лабе по С++.

CUDA вполне себе поддерживает C++. Как минимум лет 7. Может, не полностью, но в документации сейчас язык уже называется "CUDA C++" или "CUDA C/C++".

UFO just landed and posted this here
Другой неплохой путь понимания указателей — это посмотреть на их реальный смысл, память и машинную арифметику. Практика! Структуры данных. Вот прямоугольник, там лежит число — это int. Т.е. область памяти, коробочка такая, нули и единицы которой сами по себе мы интерпретируем как число. Теперь нам надо несколько таких чисел сохранить, прямоугольники в памяти идут один за другим — массив. И ещё одна дополнительная переменная, тоже блок памяти маленький, где записан адрес первого элемента массива. Теперь представим, что таких массивов — несколько. Например, их пять. Они разной длины, кстати. Адреса всех не сохранишь в одной переменной. Создаём массив длиной пять, где будут лежать адреса этих массивов. Какой тип элемента в каждом из пяти массивов? Int. Какой тип каждого элемента в массиве верхнего уровня, хранящего адреса тех пяти массивов? Int*. А для работы с этим самым верхним массивом адрес его первого элемента мы запишем в переменную какого типа? Правильно, int**. Если ещё все это на бумажке нарисовать, то будет ещё понятнее, и главное — не надо ломать мозг лингвистическими закорючками, вы видите практическое применение.
P.s. я понимаю, что в it много самоучек. В вузе на курсе С нас год муштровали машинной арифметикой, СДиА, битовыми операциями и пр. Считаю, это надо понимать, а если вы пишите на языке, где есть указатели — то тем более. И если в вузе не отложилось, надо наверстать.
Да, мне в свое время также объясняли, но к пониманию это не приблизило. Посему я решил для себя что пользоваться ими не буду, от греха подальше. Благо и не пришлось потом.
А понимание пришло откуда не ждали — когда в качестве хобби решил заняться программированием под AVR и для этого освоил ассемблер. Все сразу стало куда понятнее. Я не утверждаю что студентам нужно в обязательном порядке вдалбливать ассемблер, но как лирическое отступление при объяснении указателей можно было бы.

Примерно так нам и объясняли в вузе. И не припомню проблем с пониманием.


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

UFO just landed and posted this here
Указатели — зло. Деньги — зло.
Но зла не хватает. Все языки используют указатели, но не все в этом признаются.
И это ещё тема ссылок на указатели и указателей на ссылки не раскрыта.
С ними было бы что-то вроде:

Не проказничали чтобы
Гомофобофилофобы,
Скоро всех возьмут на вилы
Гомофилофобофилы.
В статье поднимается вопрос непонимания начинающими темы указателей и предлагается решать это объяснением синтаксиса (посредством странной аналогии).
Для объяснения темы указателей как таковых существует множество более удачных аналогий, часть из которых приведена комментаторами выше (хотя мне искренне не понятно, что может быть непонятного в определении из условной Викидепии).
Для объяснения же синтаксиса существуют документация, туториалы и т.д.
Объяснять синтаксис технического языка на примере конструкции из живого — это, как минимум, странно и долго. Как максимум — отпугнёт и разочарует.

Но за попытку спасибо)
На практике не так страшны сами указатели, как синтаксис их объявления.
Здесь нужно понимать несколько моментов.

С одной стороны, да, всё всегда упирается в синтаксис, поскольку работаем мы [программисты] с кодом; понятие «переменная» и «указатель» справедливы только для кода, непосредственно железо в этих понятиях не нуждается и ими не оперирует.
Поэтому формально я с вами согласен, да.

С другой же стороны, если посмотреть на ситуацию комплексно, то мы сможем свести все причины проблем с указателями всего-то к двум:
  1. Непонимание сути указателей;
  2. Незнание синтаксиса.

Так вот суть в том, что решить первую проблему лингвистическими аналогиями — невозможно. Здесь нужно просто объяснять человеку, что есть память, что есть переменная. Когда если человек разберётся с этим, тогда вопрос сложности восприятия синтаксиса стоять не будет, так как синтаксис не нужно понимать, синтаксис нужно просто выучить. Вне зависимости от языка (я сейчас даже не о технических, не об ЯП — обо всех: русский, английский, etc.) синтаксис — это аспект, который не требует понимания, он требует только зубрёжки.
Но здесь мы сталкиваемся со второй проблемой: вы можете зубрить что угодно и сколько угодно, но пока вы не поймёте сути темы — толку ноль.

Именно поэтому у людей и возникают проблемы с такими казалось бы несложными вещами как указатели: они либо путают порядок, в котором должно обучаться, либо выполняют лишь одно из двух действий.

Резюмируя, статья интересная, правда. Для тех, кто итак всё понимает.
Новичкам же от неё толку будет ноль, поскольку она не решает ни одной из вышеперечисленных проблем)

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

Я не сформулировал никакого чёткого правила и не сказал про указатели ничего нового.
Спасибо за статью, очень познавательно и очень интересно.

Если надо дело доходит до ** или не дай бог Страуструп *** гораздо читабельней сделать


typedef Object* ObjectPtr

Особенно удобно если потом это всё еще и в массив кладется, и не надо думать что будет когда операторы [] и * стоят в одном выражении.

А вообще спасибо, весёлая аналогия получилась :)
Ещё можно про const упомянуть:


const char* x;
// vs
char* const x;

тоже справа налево раскрывается.

По инерции начал было соображать, как раскрутить Страуструп***
Что бы понять что такое указатели, нужно сначала получить ASM и его разные аресации. Знание, что "нужно положить адрес этой строчки в DX перед вызовом INT 21h" очень помогает. Во всяком случае помогло мне — когда вся группа в универе сидела и втыкала что же такое указатели, мне все были понятно с полуслова. Что интересно, курс ассемблера x86 у нас тоже был, но позже курса Си…
Это помогает только для самых примитивных случаев.
«Настоящий» же ад указателей — это когда надо раскручивать по спирали, а не просто справа налево.
Жалко Люду. Максимально сократим её возможное присутствие в пищевой цепочке:
людоведаедовед
При условии, что людоведа зовут например Вася, а его жену не Люда.
Sign up to leave a comment.

Articles

Change theme settings