Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

Признаки устаревшего языка

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

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

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

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

Признаки устаревшего языка

Однако появляются новые языки. Но они, зачастую, ничем не лучше старых. Они ещё не «сошли с конвейера», а уже устарели. Или сошли, и в штампе об изготовлении стоит совсем свежая дата, но... Этот свежий автомобиль имеет устаревшие подвеску и тормоза, у него нет подушек безопасности, движок у него карбюраторный, а салоне — магнитола с кассетами и диапазоном «СВ» и «ДВ».

Так и язык — он только что вышел из-под пера (простите, клавиатуры) программиста, для него даже тесты ещё не написаны — а он уже устарел. Эх, не получилось никого удивить, вся публика выпячивает губы: «Мы это уже видели».

Конечно, цели создания языков могут быть разные, например Вы — завсегдатай выставок ретроавтомобилейкомпиляторов и «олдскульных» языков. Эти новые языки, которые лишь хорошо забытые старые, представлены в большом ассортименте в этих списках (разработки наших организаций и частников). Перед этими новыми языками вроде бы ставилась задача пододвинуть старые, но что делать, если они такие же, как и старые, но при этом ещё и «мясом» не обросли? Вот и курят они в сторонке, ожидая, что их компания будет пополнена другими образцами «нового слова в программировании». Такая картина наблюдается не только в родных палестинах, но и в Кремневой долине тоже. Ни Go, ни Swift точно не пошли дорогой революции.

Рискую повторить классификацию Борхеса, но попробую описать формальные критерии отнесения того или иного языка к устаревшим. И так, каковы они, эти признаки или приметы? Это когда в языке есть:

И когда в языке нет:
  • приоритетов операций а-ля Lisp или Forth;
  • контроля границ массивов а-ля Си;
  • контроля возможного переполнения при арифметических операциях;
  • системы обработки ошибок, носящий обязывающий, а не факультативный характер;
  • возможности вернуть из функций объекты как скалярные, так и не скалярные, как фиксированного, так и переменного размера;
  • функций — объектов первого класса;
  • оператора «for each»;
  • программирования в стиле доказательств;
  • вывода типов;
  • зрительных ориентиров в тексте, позволяющих отличить операции от операндов;
  • синтаксически обособленных чистых функций.

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

Иногда языки могут «выстрелить» благодаря какой-то фишке, которой нет ни у кого. Например, у языка 1С — это русские идентификаторы. Или у PHP — ориентированность на Web. Но не раз повторённое новшество теряет притягательность. Однако найти такую фишку при неизменном фундаменте программирования сложно.

Эволюционный путь развития языков «в возрасте» приведёт в итоге в тупик. В языке С++, к примеру, с каждое нововведение увеличивает число правил, усложняет язык. Чем сложнее язык, тем сложнее компилятор. Тем меньшим коллективам разработчиков он под силу. Тем меньше конкурирующих компиляторов мы будем иметь. При отсутствии конкуренции начнает страдать качество компиляторов.

Опубликовано: 2019.09.05, последняя правка: 2024.04.21    13:22

ОценитеОценки посетителей
   ██████████████ 5 (31.2%)
   ██████ 2 (12.5%)
   ██████ 2 (12.5%)
   ███████████████████ 7 (43.7%)

Отзывы

✅  2019/09/10 10:17, kt          #0 

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

Я столкнулся с этим ещё в середине 90-х. В первой прочитанной мною брошюре по Паскалю введение начиналась словами: наконец-то, после «пудовых» описаний PL/1 и ассемблера ЕС ЭВМ, мы получили язык с описанием на нескольких страницах. Года через два наши соседи купили дистрибутив Турбо-Паскаля и упаковка весила (не поверите!) как раз 16 кг. Сам помогал тащить. Основная тяжесть — описание библиотек на хорошей бумаге. Библиотеки — это не язык? Но ведь и в «пудовых» описаниях для ЕС ЭВМ были рекомендации по оптимизации, описание ОС, файловой системы и т.д. А сам язык вполне можно было изложить на нескольких страницах.

✅  2019/09/10 16:42, MihalNik          #1 

В первой прочитанной мною брошюре по Паскалю введение начиналась словами: наконец-то, после «пудовых» описаний PL/1 и ассемблера ЕС ЭВМ, мы получили язык с описанием на нескольких страницах. Года через два наши соседи купили дистрибутив Турбо-Паскаля

Надо понимать, что речь даже может идти о продукции совершенно разных людей. Но, например, и в то время, когда Турбо-Паскаль покупали ваши соседи, и когда Вы ещё читали книгу, в языках у Вирта скобочек begin ... end давно уже не было) Очевидно, удалить их (как и ещё что-то поправить) из турбо-паскаля никакой проблемы для разработчиков не было, вполне можно было выдвигать свои условия к синтаксису языка для закупок ПО.

✅  2019/09/10 16:48, kt          #2 

Так речь не о скобочках, а о возрастании сложности при использовании языка на производстве. "Чистый" Паскаль, Лисп и другие годятся только для школьной информатики. И чем дольше используется язык в реальных задачах, тем больше он обрастает всякими возможностями и дополнениями. А потом дополнениями дополнений.

✅  2019/09/10 18:36, MihalNik          #3 

Так речь не о скобочках, а о возрастании сложности при использовании языка на производстве.

А при чем тут сложность языка? 1с — сложный язык? Нет, это реально старый "бейсик". Программировать на 1с сложно? Да, это требует знания бухучета и кучи соответствующих модулей 1с-а. Но ведь бухучет — это не язык программирования, а прикладная область знания. Делфи сложный язык? Нет, но объектные модели вроде VCL там не маленькие, а это куча английских слов + их назначения и связи м/у ними в программной модели. Ни разу не видел, чтобы кто-то жаловался на добавление цилка "for each" в Delphi или операторов типа "+=" во FreePascal. Программы стало писать проще? Да. Способствует снижению дублирования лексем? Да. Что они там в компиляторе могли чересчур усложнить?
"Обрастание мясом" бычка и "приделывание костылей" к инвалиду — сильно разные явления.

✅  2019/09/10 21:10, kt          #4 

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

✅  2019/09/10 22:25, MihalNik          #5 

А тем, кто начинает осваивать с нуля сложнее — они сразу сталкиваются со всем многообразием возможностей.

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

✅  2019/09/12 11:36, Автор сайта          #6 

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

«Обрастание мясом» — это обзаведение кучей полезных вещей (типа библиотек) вокруг языка, а не в самом языке. От появления библиотек не увеличивается сложность языка, нет необходимости дорабатывать компилятор. «Всё сложно» и «всего много» становится тогда, когда меняют язык и усложняется язык. Документация же может весить 16 кг не к самому языку, который может быть очень мал, а к библиотекам, среде и проч., которые могут быть велики.

"Чистый" Паскаль, Лисп и другие годятся только для школьной информатики.

Если не сопровождаются библиотеками и прочим. А при их наличии они могут применяться много где, и в производстве тоже. Это в PL/1 много всего, что зашито прямо в язык. В других языках можно, не трогая язык, «обрастать мясом».

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

Глупо бороться с goto в старых языках. Этот оператор порою предлагает наиболее экономичные решения — в силу правил языка, которые у старых языков уже отлиты в бронзе. Бороться с памятниками — плохая идея. Но в новых языках можно построить правила так, что goto и близко будет не нужен. Однако в них порою предлагаются устаревшие подходы. Вот тогда смотришь на такой язык и приходит в голову классическое: «Молодая была немолода».

✅  2019/09/15 14:25, Александр Коновалов aka Маздайщик          #7 

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

Устаревший язык — язык, в основе которого лежат устаревшие концепции, а современные концепции отсутствуют.

Зрелый язык — язык с развитой экосистемой: сообществом, библиотеками, инструментарием. На зрелых языках написаны огромные массивы кода, который должен оставаться работоспособным.

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

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

Но поскольку язык активно используется, расширения должны сохранять обратную совместимость, поэтому максимум что доступно — рекомендовать новые практики взамен старых. Например, в современном C++ не рекомендуются «сырые» указатели (T*), вместо них рекомендуется использовать классы «умных» указателей (std::shared_ptr<T>, std::unique_ptr<T>), а вместо выделения массивов объектов malloc()/new[], нужно использовать библиотечные контейнеры (std::vector<T>, std::list<T> и т.д.)

Устаревшие средства иногда можно удалить из стандартной библиотеки (например, gets() в последних стандартах Си и Си++ удалили), но из ядра уже не удалишь — унаследованный код должен продолжать работать.

Так что любой «зрелый» язык неизбежно будет «старым», т.е. будет допускать возможность писать код по устаревшим принципам. Но есть примеры, когда новая редакция языка предлагает некоторую директиву, которая запрещает в последующем тексте использовать (некоторые) устаревшие средства. Например, "use strict" в современном JavaScript, <!DOCTYPE html> для HTML5. Довольно любопытный способ сохранить «зрелость», избежав «старины».

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

✅  2019/09/15 19:32, kt          #8 

В целом соглашусь.

Например, "use strict" в современном JavaScript

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

✅  2019/09/15 21:18, Александр Коновалов aka Маздайщик          #9 

Visual Basic (классический, VBScript, VBA, не уверен про VB.NET) имеет директиву с похожим именем и точно таким же смыслом — Option Explicit. В BASIC’е раньше тоже переменные неявно объявлялись.

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

✅  2019/09/16 12:26, Автор сайта          #10 

Запрет написания кода по устаревшим принципам по идее должен снимать остроту проблемы устаревшего кода. Однако компиляторы должны должны поддерживать все подходы — и старые, и новые. А это ляжет тяжким грузом на компиляторы.

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

Вообще-то последним словом в этой теме считается вывод типов, внешне напоминающий неявное объявление:
a = 2      // первое присваивание: присвоено и значение, и тип
Тип объекта выводится при первом присваивании. Бонусом является то, что объекту присваивается не только тип, но и значение, т. е. мы не можем использовать объект в правой части выражения до того, как ему будет присвоен тип и значение. Т.е. при выводе типов невозможно использование неинициализированных объектов. А это прогресс, большой шаг вперёд. Однако в определении функций типы должны оставаться.

✅  2019/09/16 13:06, kt          #11 

a = 2 // первое присваивание: присвоено и значение, и тип

А мне это не кажется лучшим решением. Например, в большей части моих задач исходные данные откуда-нибудь читаются и полно переменных, которые вычисляются выражениями, где встречаются и целые и вещественные.
И краткость исходного текста становится каким-то фетишем. Я ведь ещё застал времена, когда при исправлениях вырезали бритвой дырки на перфокарте и заклеивали ненужные.
Тогда старались сделать текст покороче. Я сам использовал букву «Й» для обозначения переменной. Но сейчас экономить на паре строк? Предпочитаю явно указать, что а — это вещественное с мантиссой 53 разряда, чем случайно написать
a=2
вместо требуемого
a=2e0
и получить не обнаруживаемую сразу ошибку

✅  2019/09/16 18:13, MihalNik          #12 

Однако компиляторы должны должны поддерживать все подходы — и старые, и новые. А это ляжет тяжким грузом на компиляторы.

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

✅  2019/09/16 18:38, MihalNik          #13 

Но сейчас экономить на паре строк?

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

✅  2019/09/16 21:03, kt          #14 

Как раз между прописанными типами все зависимости очевидны и не меняются. А если тип зависит от типа присваеваемой константы, вот тогда, как раз и могут при изменениях появится неочевидные зависимости.

✅  2019/09/16 21:06, Автор сайта          #15 

Предпочитаю явно указать, что а — это вещественное с мантиссой 53 разряда

Ну так у «2» есть тип по умолчанию. Если он не устраивает, можно у записать что-то типа
a = long double (2);
Часть же объектов получает тип и значение от аргумента с объявленным типом:
int fun(int x, int y) {
a = x;
. . . }
Ещё одна часть получает тип и значение от функций:
a = f (x);
Везде есть информация о типе. Когда же хочется указать её в явном виде, то в примере
a = long double (2);
это так и делается (так называемая «синтаксическая соль»).

Но сейчас экономить на паре строк?

Дело не столько в экономии, сколько в том, что инициализация типа и значения происходит одновременно. После этого не приходится бороться с неинициализированными переменными. Уходит целый класс ошибок, а это дорогого стоит.

и получить не обнаруживаемую сразу ошибку

Компилятор должен выдать ошибку, обнаружив несоответствие типов. Ведь объекты как-то дальше используются, они же не в вакууме.

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

MihalNik, что-то я не понял этой мысли.

✅  2019/09/16 23:25, MihalNik          #16 

Как раз между прописанными типами все зависимости очевидны и не меняются.

Типы-то как раз меняются, потому что часто слишком жестко привязаны к машинным архитектурам и внешним взаимодействиям.

что-то я не понял этой мысли.

А что тут непонятного? Компилятор выдает предупреждения о необходимости исправления до момента X. После X ругается, требуя правок и/или ссылаясь на старую версию компилятора. Непонятно как раз зачем тянуть десятилетиями дыры и костыли.

✅  2019/09/17 11:11, kt          #17 

Ну так у «2» есть тип по умолчанию. Если он не устраивает, можно у записать что-то типа
a = long double (2);
Везде есть информация о типе. Когда же хочется указать её в явном виде это так и делается (так называемая «синтаксическая соль»).
Дело не столько в экономии, сколько в том, что инициализация типа и значения происходит одновременно. После этого не приходится бороться с неинициализированными переменными. Уходит целый класс ошибок, а это дорогого стоит.

Разумеется, я привык к своему болоту и как всякий порядочный кулик должен его хвалить. И мне такие вещи вроде вывода типа кажутся странными. С моей точки зрения «2» не имеет вообще никакого типа, это заполнение значением. И, кстати, а как по константе помимо типа определить ещё и разрядность (в битах или байтах)?

Например, какую взять разрядность для «а» в выражении «а=0»? Ей-богу, проще самому явно указать и тип, и разрядность напрямую для каждой переменной. Лично мне так и проще, и понятней (я хозяин своих переменных!). Явная разрядность отсутствует во многих современных языках? Ну и зря. Ведь уже нет архитектур с не 8-разрядными байтами. Значит разрядность — универсальная характеристика для выделения памяти.

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

С неинициализированными надо бороться, как 50 лет назад. БЭСМ-6 была 48-разрядной машиной, в каждой ячейке можно было написать или целое или вещественное или 6 символов или 2 команды. Реально БЭСМ-6 была 50-разрядной. Еще два разряда указывали — это данные или команды. При запуске программы память для данных расписывалась этими разрядами и при попытке обратиться к неинициализированной переменной происходило аппаратное исключение (попытка выполнения команд как данных). Видите, у нас полвека назад уже боролись с вирусами по переполнению стека ))

✅  2019/09/18 11:40, Автор сайта          #18 

я привык к своему болоту

Ага, к отеческим гробам, родному пепелищу, а далее по тексту.

С моей точки зрения «2» не имеет вообще никакого типа, это заполнение значением. И, кстати, а как по константе помимо типа определить ещё и разрядность (в битах или байтах)?

Мне видится, что тип «2» должен быть определён как «беззнаковое целое длиною 2 бита». Подобно LLVM IR. А уж потом компилятор решает, сколько байтов должна занять ячейка, хранящая это число.

какую взять разрядность для «а» в выражении «а=0»?

Это «беззнаковое целое длиною 1 бит».

разрядность — универсальная характеристика для выделения памяти.

Полностью согласен.

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

А конкретнее можно? А то ведь некоторые сторонники goto тоже говорят: «Алгоритм та-а-акой сложный, что без goto не обойтись». Но на поверку оказывается, что точный научный подход может преодолеть любую путаницу. И в случае с неинициализированными переменными можно спроектировать новый язык (старых языков это не касается) так, что они в принципе невозможны.

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

Может, я что-то путаю, всё-таки я знаком с этой системой только по статьям. Но мне кажется, что там предотвращалось выполнение кода там, где лежат данные. И наоборот, команды могли читать и писать данные, но никак не сами команды. Тогда это другая картина: неинициализированные данные имеют такие же дополнительные (49, 50) биты, что и инициализированные данные. Но я могу и заблуждаться, я с этой техникой не работал.

Но и тут можно увидеть преимущество вывода типов. БЭСМ и «Эльбрусы» увидели бы ошибку только в рантайме, а вывод типов работает уже во время компиляции!

✅  2019/09/18 15:18, kt          #19 

инициализация типа и значения происходит одновременно.

Это при линейной структуре исходного текста. А если стоят ветвления? Например:
if x>0 then a=1; else a=1e-4;

Это «беззнаковое целое длиною 1 бит».

А пользы от такого определения?
Вот в реальной программе:

ON ENDFILE BEGIN; X=0; GOTO ДАЛЬШЕ; END;

GET LIST(Y);
IF Y>0 THEN X=FLOOR(Y); ELSE X=CEIL(Y);
ДАЛЬШЕ:
Чем мучится с пониманием, сколько здесь разрядов нужно под X, не проще ли самому их указать, пусть и с запасом?

неинициализированные данные имеют такие же дополнительные (49, 50) биты, что и инициализированные данные.

Давно было, может тоже память подводит. Но вот как-то так:

Длина слова -- 48 двоичных разрядов и два контрольных разряда (четность всего слова должна была быть "нечет". Таким образом, можно было отличать команды от данных -- у одних четность полуслов была "чет-нечет", а у других -- "нечет-чет". Переход на данные или затирание кода ловилось элементарно, как только происходила попытка выполнить слово с данными).

Четность-нечетность проверялась при ЧТЕНИИ из ячейки. Таким образом, нельзя было выполнить данные и прочитать команды. Память расписывалась командами ЗЧ (кажется) и попытка чтения до записи вызывала исключение.

БЭСМ и «Эльбрусы» увидели бы ошибку только в рантайме, а вывод типов работает уже во время компиляции!

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

✅  2019/09/19 11:24, Автор сайта          #20 

А если стоят ветвления?

Это решаемая проблема. Не в PL/1, конечно, а в новом языке, который можно всё это предусмотреть. Даже с циклами решаемо.

Чем мучится с пониманием, сколько здесь разрядов нужно под X, не проще ли самому их указать, пусть и с запасом?

В LLVM IR так и пишут:
ui1  value;     // величина, под которую необходимо 1 бит
А сколько конкретно будет выделено? Зависит от того, под какую платформу идёт компиляция. Но если есть желание узнать, то для этого есть функция периода компиляции sizeof.

Давно было, может тоже память подводит.

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

А здесь анализ при компиляции может ошибку и не увидеть.

В функциональной/декларативной парадигме программирования всякого рода переменные состояния задвинуты в угол и маргинализированы. Тут просто другая философия программирования.

✅  2019/09/19 12:51, Александр Коновалов aka Маздайщик          #21 

Вообще-то последним словом в этой теме считается вывод типов, внешне напоминающий неявное объявление:

Подчёркивание моё. Я имел ввиду то, с чем борятся implicit none в Фортране и Option Explicit в Бейсике. В этих языках если где-то используется необъявленная переменная (в любом месте программы), то вместо выдачи синтаксической ошибки компилятор её неявно объявляет. Молча. И, возможно, даже не инициализируя.

В статически типизированных языках, где есть вывод типов, обычно есть оператор объявления переменной. Синтаксис оператора допускает либо указывать тип, не указывая начального значения, либо указывать начальное значение без типа. И этот оператор синтаксически отличается от присваивания. Выглядит это по-разному:
auto pi = 3.142;   /* C++ */
var pi = 3.142; /* C#, Go */
pi := 3.142 /* Go, синтаксический сахар для var pi = 3.142 */
В скриптовом языке Python первое присваивание переменной объявляет её. Это значит, что если используется переменная до первого присваивания, то программа вылетает с синтаксической ошибкой.
print(x) # ошибка
x = 1
print(x) # выведет 1
Это как бы некоторый компромисс между контролем ошибок и динамической типизацией.

А вообще, польза от неявного определения типа может быть там, где эти самые типы довольно многословны. Например, нужно получить откуда-то класс и вызвать у него два метода (псевдокод):
VeryCoolLibrary.Widgets.Window w = globalContext.getMainWindow();
w.setVisible(true);
w.redraw();
Тип для w можно вывести во время компиляции и тем самым сократить код:
var w = globalContext.getMainWindow();
w.setVisible(true);
w.redraw();

✅  2019/10/26 09:48, utkin          #22 

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

На самом деле существует куча задач (конечно отличных от лабораторных примеров 2 + 2), где присвоение типов и значения не очень хорошо в плане производительности. И ленивые вычисления тому хороший пример, естественно при правильном использовании. Это может использоваться всегда, когда идет общение с внешним миром (сеть, диск, флешка), там где программа не всегда может контролировать ресурсы. Тогда фактическое присвоение значений требуется по факту использования, а вот объявление может быть необходимо заранее. Если Вы намереваетесь тянуть огромную структуру из файла (например, ФИО, зарплата по месяцам, отчисления в фонды, стаж, время болезни и пр. и пр.), то каждая инициализация будет утомительна. Если у Вас не одна анкета, а сотни? Обычное явление — среднее по размерам предприятие. Поэтому тут сложно утверждать однозначно, что это правильно. Это правильно, когда у Вас в статике висит Int. А когда Вы динамически решаете задачи практического характера, все это не факт, что полезно, а иногда и вредно.

Например, какую взять разрядность для «а» в выражении «а=0»? Ей-богу, проще самому явно указать и тип, и разрядность напрямую для каждой переменной. Лично мне так и проще, и понятней (я хозяин своих переменных!) Явная разрядность отсутствует во многих современных языках? Ну и зря. Ведь уже нет архитектур с не 8-разрядными байтами. Значит разрядность — универсальная характеристика для выделения памяти.

Это называется поклонение машинам и рабство программиста :). Разве в алгоритме расчета затрат муки на приготовления пирожка с капустой нужно знать разрядность? О чем это Вы :) ? Ради машины Вы готовы применять кучу знаний, которые совершенно не требуются для решения задачи. Игры разума.

Мне видится, что тип «2» должен быть определён как «беззнаковое целое длиною 2 бита».

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

Это «беззнаковое целое длиною 1 бит».

А надо выделить одно машинное слово, просто потому что так быстрей на всех этапах работы.

Я имел ввиду то, с чем борятся implicit none в Фортране и Option Explicit в Бейсике. В этих языках если где-то используется необъявленная переменная (в любом месте программы), то вместо выдачи синтаксической ошибки компилятор её неявно объявляет. Молча. И, возможно, даже не инициализируя.

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

Тип для w можно вывести во время компиляции и тем самым сократить код:

Да, но явное указание типа позволяет обратиться не к данному типу, а ещё и к его родителю (если класс/метод наследуется от одного предка), что дает некоторую гибкость. То есть это не просто ритуал, а дополнительная возможность (вызвать по факту и вызвать предка).

✅  2019/10/26 10:00, utkin          #23 

Иногда языки могут «выстрелить» благодаря какой-то фишке, которой нет ни у кого. Например, у языка 1С — это русские идентификаторы.

Изначально 1С "выстрелил" потому что ниша была свободна. Сейчас все работают из-за фактического монополизма и безысходности, а не от шикарности языка или от того, что там русские буковки.

✅  2019/10/26 10:10, utkin          #24 

Эволюционный путь развития языков «в возрасте» приведёт в итоге в тупик. В языке С++, к примеру, с каждое нововведение увеличивает число правил, усложняет язык. Чем сложнее язык, тем сложнее компилятор. Тем меньшим коллективам он под силу. Тем меньше конкурирующих компиляторов мы будем иметь. При отсутствии конкуренции начнает страдать качество компиляторов.

Эволюционный путь развития языков приводит ровно к тому же, что и эволюционный путь развития биологических организмов — к специализации. Си, например, вполне эффективно используется в микроконтроллерах и выковырять его оттуда каким-нибудь Питоном/Бейсиком/С# можно, но проблематично. На PHP есть все библиотеки для того чтобы писать десктопные, обычные программы для персоналок. Но этого стараются не делать. JavaScript вон тоже активно наступает и уже не просто работа в браузере, а все на свете. Фортран также до сих пор используют (например, для вычислений на видеокартах). Поэтому тупик это только один из возможных вариантов.

✅  2019/10/26 14:47, kt          #25 

Разве в алгоритме расчета затрат муки на приготовления пирожка с капустой нужно знать разрядность? О чем это Вы :) ? Ради машины Вы готовы применять кучу знаний, которые совершенно не требуются для решения задачи. Игры разума.

Я имел ввиду, что лучше явно указать разрядность, чем писать все эти совершенно непонятные "double" и "long", размер которых неочевиден. В PL/1 разрядность указывалась с самого начала и это делало язык как раз независимым от архитектуры машины.

✅  2019/10/28 08:17, utkin          #26 

Я имел ввиду, что лучше явно указать разрядность, чем писать все эти совершенно непонятные "double" и "long", размер которых неочевиден. В PL/1 разрядность указывалась с самого начала и это делало язык как раз независимым от архитектуры машины.

Так разрядность тоже не очевидная вещь для задачи с мукой и пирожками. Мне не нужна разрядность, мне нужно знать сколько вешать муки в граммах. Чувствуете разницу? Каждая кухарка должна иметь возможность стать программистом :). Опять же, Вам для работы потребуется 13 разрядов, и? Все равно компилятор переведёт для одной машины в 4 байта, а для компиляции в другой системе в зависимости от параметров в 4 или в 8 байт. И никак Вы на этот процесс повлиять разрядами не сможете. И это правильно, потому что для решения поставленной задачи знать такие вещи не только не нужно, но и вредно. Как у военных — минимум доступной информации для исполнителя. Только та, что реально требуется для решения поставленной задачи.

И второе размышление. К задачам следует относиться серьёзней, чем это обычно принято. А именно проектировать решение перед непосредственным кодированием программы. В математике есть такая вещь, как исследование функции, и там есть поиск экстремумов (минимального и максимального значения). Здесь проводится тоже самое и укладывается в стандартный тип данных — Long и прочее. Указание разрядности в данном случае просто лишнее звено при пересчёте данных. Почему уже писал — тот же байт (то есть допустим, для решения Вам требуется 7 разрядов) ни один адекватный современный компилятор Вам не даст, при условии, что Вы действительно не будете работать с разрядами напрямую с помощью специальных операций. Вам дадут машинное слово — минимум два байта (а то и 4). Потому что это эффективно, то есть оптимально в соотношении память/скорость исполнения. Даже если Вам и кажется это полным расточительством. И более того, память под статические структуры также выделяется странично (то есть не только в куче, но и для статичных записей).

В результате, если Вы там насчитаете допустим 121 разряд на всю Вашу структуру данных, компилятор все равно выделит Вам 160 разрядов, независимо от того, нравится Вам это или нет. Потому что он сделает округление на границу выделенной страницы памяти. А она кратна опять же в зависимости от компилятора и/или платформы — 8/16/32 байтам. Все остальные разряды, как и при стандартном определении Long, Integer и т.д., будут лежать мертвым грузом. Про Bool уже писал ранее — минимум 2 байта, даже если Вам требуется один разряд. Потому что так эффективно — это такой баланс между затратами и результатом.

✅  2019/10/28 10:08, kt          #27 

По-моему, мы опять друг друга недопоняли.

Я писал не о необходимости самому рассчитывать число разрядов, а о форме явного указания длины объектов в памяти. В PL/1 это делается не очень удобно в битах, а не в байтах. Это было сделано «на всякий случай». Вдруг где-то будут не 8-разрядные байты :)

В этом смысле фортрановское REAL(8) даже понятнее и проще, чем FLOAT(53) из PL/1. И уж явно лучше безобразной формы типа «long long», из которой вообще непонятно, сколько же байт выделено.

А как куратор и частично разработчик транслятора PL/1 я хорошо понимаю, сколько и когда выделяется транслятором байт. И иногда даже «кривые» разряды могут принести пользу — конечно байт все равно выделится целое число, но можно при трансляции проверить, например, помещается ли константа в эти кривые разряды. Такая проверка в общем случае недостаточна, зато очень дёшева.

А для избегания штрафных тактов при обращении к объектам в х86 требуется их выравнивание кратное длине объекта (так и делает PL/1-KT). Т.е. 2-х байтные объекты выравниваются в памяти на 2, 4-х байтные объекты — на 4, 8-ми байтные на 8. Таким образом, в х86 для bool выделится все-таки один невыровненный байт, при том, что Intel физически при обращении всегда читает не менее 4 байт.

✅  2019/10/28 11:27, MihalNik          #28 

В PL/1 это делается не очень удобно в битах, а не в байтах.

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

✅  2019/10/28 17:49, utkin          #29 

Вдруг где-то будут не 8-разрядные байты :)

Строго говоря, нет никакого стандарта, в котором байт равен 8 битам. И в истории были системы, в которых байт содержал иное количество бит. Просто это общепринятое явление в данный конкретный момент развития ИТ. И не факт, что позже с развитием оптических или квантовых технологий эта величина не изменится.

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

Разве в данном случае это не одно и тоже? Чтобы указать длину объекта, её нужно сначала рассчитать.
Допустим, имеем а=0. Какова здесь длина объекта? Очевидно, что данной информации недостаточно, потому что мы не имеем информации об области допустимых значений для переменной а. Это может быть константа, это может быть значение, которое уложится в 8 бит, а может быть там нужна длинная арифметика (например, для операций шифрования).
То есть рассчитывать число разрядов необходимо всегда. Просто сейчас это делается не детализировано — до бита, а блочно — Int, Long, Bool. А уже конкретная реализация определяется платформой — компилятором, процессором и операционной системой.

А для избегания штрафных тактов при обращении к объектам в х86, требуется их выравнивание кратное длине объекта (так и делает PL/1-KT). Т.е. 2-х байтные объекты выравниваются в памяти на 2, 4-х байтные объекты — на 4, 8-ми байтные на 8. Таким образом, в х86 для bool выделится все-таки один невыровненный байт, при том, что Intel физически при обращении всегда читает не менее 4 байт.

Это очень старые данные. Даже старые Дельфи (еще до Эмбекадеро) уже во всю равнялись на 16 байтовые страницы. Очевидно, что со времени расцвета PL/1 мир значительно продвинулся вперед. В 64-х битных системах (а они медленно, но верно наступают на все вокруг) выравнивание даже на 4 байта очень не эффективно.

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

Как раз для сборки мусора и сделан страничный, а не объектный доступ к памяти. Без такого подхода процессы будут протекать ещё медленней. Это если и не оптимальная система работы с памятью, то она очень близка к оптимальной, учитывая что приходится иметь дело с различными системами. Некая вещь, которая хорошо работает с имеющимся зоопарком проблем.

✅  2019/10/28 23:43, Автор сайта          #30 

Мне видится, что тип «2» должен быть определён как «беззнаковое целое длиною 2 бита».

Сразу же падение производительности. Все процессоры неэффективно работают с битами.

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

Во всех Бейсиках, которые мне известны, переменные всегда инициализировались.

Да, каким-то значением по умолчанию. Например, нулём для int. Но есть разница между правильным значением и значением по-умолчанию. Инициализация значением по умолчанию хороша только одним: выключается «датчик случайных чисел» при чтении неинициализированной переменной. А вывод типов исключает само чтение неинициализированного.

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

Да, но у организмов эволюция отсекает лишние и устаревшие функции, а у языков программирования — нет. Вот и тащится воз обратной совместимости.

kt: лучше явно указать разрядность

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

мне нужно знать сколько вешать муки в граммах.

Если эти граммы известны компилятору как константа, то он вычисляет необходимое и достаточное количество битов. Если это читается в рантайме из БД, то это делает программа.

Строго говоря, нет никакого стандарта, в котором байт равен 8 битам. И в истории были системы, в которых байт содержал иное количество бит. Просто это общепринятое явление в данный конкретный момент развития ИТ. И не факт, что позже с развитием оптических или квантовых технологий эта величина не изменится.

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

✅  2019/10/29 07:42, MihalNik          #31 

Но есть разница между правильным значением и значением по-умолчанию. Инициализация значением по умолчанию хороша только одним: выключается «датчик случайных чисел» при чтении неинициализированной переменной. А вывод типов исключает само чтение неинициализированного.

Принципиального отличия нет, любое изменение переменной в условии даст подобное состояние, т.е. локально значение до условия будет каким-то "по умолчанию".

✅  2019/10/29 10:02, utkin          #32 

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

Тогда зачем её вообще объявлять, если от объявлений нет никакого практического смысла? Это снова программирование программиста, а не компьютера.

Да, но у организмов эволюция отсекает лишние и устаревшие функции, а у языков программирования — нет. Вот и тащится воз обратной совместимости.

У Питона две версии живут свой собственной жизнью — дивергенция в биологии. Собственно синтаксис С++ раскиданный по всем языкам это тоже эволюция без обратной совместимости. Так что я тут не совсем согласен. Да и в природе были мастодонты, но они вымерли. А осталось приспособленность. И мы видим на примере специализации Си/С++ в микроконтроллерах и на развитии несовместимых версиях Питона, что эволюция это общий фактор развития систем.

Если эти граммы известны компилятору как константа, то он вычисляет необходимое и достаточное количество битов. Если это читается в рантайме из БД, то это делает программа.

В том то и дело, что компилятор будет использовать практически всегда избыточное количество битов. Потому что биты нельзя насыпать в стакан. Они выдаются пачками — байтами и словами. Поэтому надо вам 7, Вы получите 8 минимум. Надо 17, получите 32 (или 24 смотря какой размер страницы).

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

Bool или Int как раз этим самым и занимаются. Нет необходимости плодить лишнюю сущность — скальпель Оккама и все такое.

✅  2019/10/29 10:17, utkin          #33 

То что данный подход эффективен подтверждают практически все файловые системы — там тоже память выделяется блоками. И стандартный для NTFS сейчас 4096. Надо Вам пароль в Блокноте записать — сколько там байт? 10-16 наберется? А на жестком диске данный файл займет 4 Кб. Работа с памятью примерно такая же, только масштабы не столь эпичны.
И работа с ФС в программе также эффективна именно блочно. Хотите проверьте экспериментально — читайте побайтно и читайте блоками, там разница видно просто невооруженным взглядом.

✅  2019/10/29 10:32, kt          #34 

Куда-то мой комментарий провалился, повторяю:

Это очень старые данные. Даже старые Дельфи (еще до Эмбекадеро) уже во всю равнялись на 16 байтовые страницы.

Не понял, о каких страницах речь. Если о строке кэша нижнего уровня, то обычно выравниваются на 16 не данные, а коды подпрограмм. В таком случае, первые 16 кодов подпрограммы (переходов там, вероятно, ещё нет) сразу после вызова подпрограммы не требует тут же ещё и чтения следующей кэш-строки. А вот зачем данные выравнивать на 16, а не кратно их длине, непонятно. Например, FLOAT(53) все равно поместится целиком в кэш-строку, независимо выровнен он на 8 или на 16.

Очевидно, что со времени расцвета PL/1 мир значительно продвинулся вперед.

Собственно язык здесь не причем. Хотя как раз в PL/1 с самого начала был атрибут данных ALIGNED, подсказывающий компилятору, где можно сократить обращения к памяти.

В 64-х битных системах (а они медленно, но верно наступают на все вокруг) выравнивание даже на 4 байта очень не эффективно.

С чего Вы это взяли и где на это ссылаются?

Bool или Int как раз этим самым и занимаются. Нет необходимости плодить лишнюю сущность — скальпель Оккама и все такое.

Как раз явное указание разрядов позволяет устранить лишние сущности. При переходе PL/1 с 32 на 64 просто FIXED(31) увеличились до FIXED(63). Безо всяких добавлений «long long»

✅  2019/10/29 12:03, Автор сайта          #35 

MihalNik: Принципиального отличия нет, любое изменение переменной в условии даст подобное состояние, т.е. локально значение до условия будет каким-то "по умолчанию".

int i;		// в Бейсике здесь присвоят 0; в Си — «случайное» число
if (i = 1) // здесь не проверка на равенство «==»,
// а присвоение и проверка i на неравенство 0
В примере выше до условного оператора значение i будет непредсказуемо для Си и 0 для Бейсика. То есть нельзя сказать, что

локально значение до условия будет каким-то "по умолчанию"

Это верно будет для Бейсика. И что Вы имели в виду под «подобным состоянием»?

utkin: компилятор будет использовать практически всегда избыточное количество битов. Потому что биты нельзя насыпать в стакан. Они выдаются пачками — байтами и словами. Поэтому надо вам 7, Вы получите 8 минимум… Bool или Int как раз этим самым и занимаются.

Занимаются похожими вещами, но не теми же. Сколько нужно бит для хранения константы 0? Ровно 1 бит. Но Вы объявляете:
int i = 0;	// длина кратна 16
Но тут Вам встречается какой-нибудь 14-разрядный квантовый вычислитель. Если бы у Вас константа была известна компилятору как однобитная, то она спокойно бы разместилась в 14 битах. Но Вы-то её объявили 16-разрядной и она не поместится в 14 разрядов! Тогда 14 разрядов будут запрошены дважды, чтобы поместить якобы 16-разрядную (а на деле одноразрядную) константу. Т.е. знание размера объекта с точностью до бита лучше знания размера, кратного 8.

Надо Вам пароль в Блокноте записать — сколько там байт? 10-16 наберется? А на жестком диске данный файл займет 4 Кб. Работа с памятью примерно такая же, только масштабы не столь эпичны.

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

Как раз явное указание разрядов позволяет устранить лишние сущности.

Абсолютно точно!

✅  2019/10/30 09:41, utkin          #36 

Не понял, о каких страницах речь.

О страницах, которые выделяют системы управления памятью. Знаете в компиляторе есть такая вещь, которая ответственна за выделение памяти и размещения в них данных. Что должно быть в стеке, что в куче, размер блока (который состоит из страниц) и т.д.

А вот зачем данные выравнивать на 16, а не кратно их длине, непонятно.

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

С чего Вы это взяли и где на это ссылаются?

Это архитектурный вопрос. Прочитать 8 байт за 1 раз эффективней, чем читать 8 раз по одному байту. Даже если конкретно вот сейчас Вам нужен только один байт.

При переходе PL/1 с 32 на 64 просто FIXED(31) увеличились до FIXED(63). Безо всяких добавлений «long long»

На это есть простая причина — совместимость.

Занимаются похожими вещами, но не теми же. Сколько нужно бит для хранения константы 0? Ровно 1 бит. Но Вы объявляете:

Во-первых, 8 бит, а не ровно 1. И это как минимум. Ну я же писал ранее. Вам никто 1 бит не даст. Вот Вы придете в магазин и скажете — дайте мне 125 гр. муки, я хочу испечь два пирожка. И? Вы можете купить 0,5 кг, 1 кг, 1,5 кг и т.д. Но не 125 гр. муки. Понимаете о чем я? Ваши хотелки о том, чтобы нуль хранился в 1 бите для аппаратной составляющей компьютера не значат ровным счетом ничего.
Во-вторых, а организация процессов по извлечению из 8 бит (подчеркиваю — минимум 8!) конкретно Вашего нулевого бита сколько будет стоить в байтах и словах? Вы считали? Сколько потребуется на это команд и сколько места они занимают?
В-третьих, давайте пойдем от противного. Вот есть псевдокод такой на виртуальном языке:

целое(32 бита) а = 45658421
целое(1 бит) б = 0

.... // Какие-то другие команды

а = а + б

И? Вы думаете Вам снова бесплатно без единого лишнего бита удасться напрямую сложить разнобитовые величины? А если там будет переменная а числом вещественным?

Но тут Вам встречается какой-нибудь 14-разрядный квантовый вычислитель. Если бы у Вас константа была известна компилятору как однобитная, то она спокойно бы разместилась в 14 битах. Но Вы-то её объявили 16-разрядной и она не поместится в 14 разрядов! Тогда 14 разрядов будут запрошены дважды, чтобы поместить якобы 16-разрядную (а на деле одноразрядную) константу. Т.е. знание размера объекта с точностью до бита лучше знания размера, кратного 8.

Все верно. Поэтому типов много и какой брать решать Вам.

Простой контраргумент — попробуйте выделить разряды для хранения кодировки UTF :).

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

Именно от этого и хотят избавиться. Большой объем информации не нужный для решения задачи как раз таки и есть барьер сложности — порог вхождения. Также Хаскелл — то же самое. Решение простых задач либо не дает преимуществ при сравнении со стандартными средствами либо просто вырвиглазно. Потому что требует какой-то левой лабуды и матана для определения количества муки.

✅  2019/10/30 11:05, kt          #37 

О страницах, которые выделяют системы управления памятью. Знаете в компиляторе есть такая вещь, которая ответственна за выделение памяти и размещения в них данных. Что должно быть в стеке, что в куче, размер блока (который состоит из страниц) и т.д.

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

Это архитектурный вопрос. Прочитать 8 байт за 1 раз эффективней, чем читать 8 раз по одному байту. Даже если конкретно вот сейчас Вам нужен только один байт.

Извините, но я уже потерял «нить понимания». Аппаратно память организована странично для создания виртуальной памяти. Размер аппаратных страниц 4096 байт или (вариант) 1 Мбайт.
Причем здесь «16 байтные страницы»? А вот аппаратный кэш организован построчно с размером строки 16 (или 32) байта. Если объект короче строки, его кэш выгодно выравнивать кратно его размеру. Например, в строке поместится ровно два «double» иначе при обращении будут лишние чтения кэш-строк. Компилятор не выделяет память «страницами», а просто назначает переменным адрес своего текущего внутреннего счетчика, обычно с предварительным выравниванием. Можно, конечно, сделать выравнивание каждого объекта программы на 16, но зачем, когда его длина всего лишь 4 или 8 байт? Никакого выигрыша скорости это не даст, все равно читается строка кэш. Наоборот, два double в одной кэш-строке могут уменьшить число обращений к памяти, но при этом они должны быть выровнены на 8, а не на 16. И при этом, например, 32-разрядный Intel имеет лишь 30 адресных линий (А0 и А1 — внутренние) и физически всегда читает по 4 байта, а потом из них внутри себя выбирает нужное.

На это есть простая причина — совместимость.

На мой взгляд, главная причина — продуманность

✅  2019/10/30 12:24, MihalNik          #38 

Т.е. знание размера объекта с точностью до бита лучше знания размера, кратного 8.

А ещё лучше знание числа, логарифм которого берется для узнавания размера в битах. Нет же смысла его считать вручную, наверное, Utkin это подразумевал. Такое число имеет больше применения — оно же может (и должно) для проверки получаемых данных использоваться. Иначе же возникает паразитная зависимость исправления одного от другого.

✅  2019/10/30 12:41, MihalNik          #39 

И что Вы имели в виду под «подобным состоянием»?

Проблемы расстояния м/у объявлением или любым присвоением до следующего обращения к переменной равносильны.
Присвоение при условии "размазывает" значение переменной по разным кускам и отобразить его грамотно — скорее задача среды разработки.

✅  2019/10/30 14:15, utkin          #40 

Извините, но я уже потерял «нить понимания».

При создании больших программ используются менеджеры памяти. Они распределяют память под данные программы. Память в современных программах давно распределяют они. И распределяют её не байтами и битами, а блоками в одну-несколько страниц. А страница уже кратна 8-16 байтам. Потому что современные процы эффективно работают с такими величинами. Вот и все.

А вот аппаратный кэш организован построчно с размером строки 16 (или 32) байта.

С аппаратными кешами там все намного сложней, там эффективность плавающая, но в целом также способствует данному состоянию — чтение потока эффективней чтения бита/байта или иной нестандартной структуре.

Компилятор не выделяет память «страницами»,

:) Именно так он и поступает. Иначе получите страшные тормоза для любой динамической работы.

но зачем, когда его длина всего лишь 4 или 8 байт?

Да, где это? В современных программах оперируют объектами. Еще в прошлом веке уже во всю использовали структуры данных (struct в C++, record в Паскале) и там также появлялись динамические поля (например, динамические массивы или массивы структур). И там размениваться на 4 байта это вверх неэффективности.
Длина в 4 или 8 байт это красиво, когда Вы пишите простую автоматизацию, типа студенческого квадратного корня. В современных программах уровень абстракции выше и потому биты там зло.

Никакого выигрыша скорости это не даст, все равно читается строка кэш.

Конвейер же. И MMX и SSE придуманы уже черти когда. Как же не даст? Потоковое чтение на уровне команд процессора.

И при этом, например, 32-разрядный Intel имеет лишь 30 адресных линий (А0 и А1 — внутренние) и физически всегда читает по 4 байта, а потом из них внутри себя выбирает нужное.

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

Нет же смысла его считать вручную, наверное, Utkin это подразумевал.

Конечно. Потому что все равно будет потом снова пересчитано.

✅  2019/10/30 16:29, kt          #41 

Я ещё больше запутался. Оказывается, имеется ввиду менеджер памяти при выполнении программы. Тогда причем здесь компилятор и какие-то «16 байтовые страницы»? Компилятор не выделяет памяти динамическим объектам и понятия не имеет, на что там выравнивается, а просто генерирует обращения к таким объектам через указатели. А я говорил о статическом распределении переменных при компиляции, кстати, обращение к ним — самое быстрое. И, поверьте, наличие или отсутствие в программе массивов структур не является признаком её современности или признаком студенческой работы. А также 30 адресных линий Intel не имеет никакого отношения к конвейеру, просто 32 разрядная память устроена так, что всегда идет выборка в 32 бита. Я соскакиваю с темы, так как мое непонимание только возрастает.

✅  2019/10/30 18:30, utkin          #42 

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

Да, здесь я выразился не корректно. Я имел ввиду, что это все учитывается при компиляции. Потому что в рантайме это будет работать так.

✅  2019/10/30 18:38, utkin          #43 

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

Речь о том, что читаются такие данные быстрей, чем просто отдельные слова и байты, потому что оптимизируются под чтение данных, расположенных непосредственно друг за другом. Отдельные переменные, даже объявленные друг за другом, совсем не факт, что будут располагаться друг за другом в памяти. Структура практически всегда располагается целиком (кроме сложных объектов) рядом. Поэтому её чтение будет быстрей за счет выполнения таких команд.

✅  2019/10/30 19:38, Александр Коновалов aka Маздайщик          #44 

Никаких «16-байтовых страниц» не существует — это выдуманный utkin’ом термин. Менеджеры памяти (тот же malloc() для Си), как правило, выделяют адреса, кратные 16 байтам.

Для эффективного чтения 32-битных значений достаточно выравнивания на 4 байта, для 64-разрядных — 8 байт. Кратность 16 байтам сделана, по всей видимости, для более эффективного использования кэша. Программист, написавший структуру размером 16 байт и выделивший массив структур, может быть уверен, что каждая структура попадёт в целую кэш-линию.

kt, а можно ли в Вашем компиляторе указать float(39) или float(13)? Что тогда получится? А fixed(13)?

✅  2019/10/31 08:58, kt          #45 

kt, а можно ли в Вашем компиляторе указать float(39) или float(13)? Что тогда получится? А fixed(13)?

Ничего хорошего не получится. Из-за жесткой привязки к FPU все переведется в FLOAT(53) и FLOAT(24). Иногда с сообщением «не сделано», а иногда и молча :(
FIXED(13) соответственно станет FIXED(15) c минимальными проверками.
Например, вот так пройдет:
 dcl i fixed(13) static init(8191);
А так уже нет:

3 c dcl i fixed(13) static init(8192);
ПЕРЕВОД ?
КОМПИЛЯЦИЯ ПРЕКРАЩЕНА

✅  2019/10/31 09:18, utkin          #46 

Никаких «16-байтовых страниц» не существует — это выдуманный utkin’ом термин. Менеджеры памяти (тот же malloc() для Си), как правило, выделяют адреса, кратные 16 байтам.

Это тоже самое и есть :). Минимальная единица в менеджере памяти страница. И она, кстати, не обязана быть равна 16 байтам, просто наиболее часто используется данная величина. И выделяются не адреса, а блоки памяти. Блок всегда имеет целое число страниц. Вот это самое я и писал между прочим. Согласен был не точен в формулировках. Но речь шла об этом. И о том, что в свете такого механизма работы с памятью прямое указание битов не даст Вам никакой эффективности в сравнении с традиционными типами данных.

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

И программист, который написал структуру в 15 байт, также может быть в этом уверен. Также как и программист, который написал структуру в 17 байт. Потому что будет две страницы в 16 байт. А не 16 байт и 1 байт.

✅  2019/10/31 11:31, Автор сайта          #47 

utkin, меня раздирает когнитивый диссонанс: Вы так смело оперируете терминами («кэш нижнего уровня», «выравнивание», «конвейер») и какими-то цифрами («16-байтовые страницы», «кратна 8-16 байтам»), но при этом пишите интерпретатор(!) языка и совсем не на ассемблере(!). В Вашем интерпретаторе делается подсчёт байтов с целью этого самого выравнивания? Так ли часто в Вашем интерпретаторе работают команды MMX и SSE, как Вы их вспоминаете всуе? Пытаясь в чём-то переубедить Дмитрия Юрьевича, не пытаетесь ли Вы «научить учёного», у которого, в отличие от Вас, есть за плечами написанный транслятор с двух языков: PL/1 и ассемблера?

Я бы мог предложить Вам отбросить словеса в сторону и написать какую-то тестовую задачу. Например, вычисление последовательности простых чисел с помощью «решета Эратосфена». Надо найти, допустим, миллионное простое число. Вы, utkin, пишите программу на своём языке, а Дмитрий Юрьевич — на PL/1. Оценка — по двум старым критериям: скорость и объём занятой памяти. MihalNik возьмёт на себя роль арбитра, он же объявит победителя в номинации «лучшее знание архитектуры процессоров и памяти».

✅  2019/10/31 12:47, utkin          #48 

Вы так смело оперируете терминами («кэш нижнего уровня», «выравнивание», «конвейер») и какими-то цифрами («16-байтовые страницы», «кратна 8-16 байтам»), но при этом пишите интерпретатор(!) языка и совсем не на ассемблере(!).

И? Это означает, что я не должен касаться этой темы и изучать ее? Или внутреннее устройство компиляторов для меня автоматически становится недоступным? Неожиданно.

В Вашем интерпретаторе делается подсчёт байтов с целью этого самого выравнивания?

За него это делают низкоуровневые инструменты.

Так ли часто в Вашем интерпретаторе работают команды MMX и SSE, как Вы их вспоминаете всуе?

Ну я как бы и собственного вертолета не имею, но это не значит, что я не могу о них говорить.

Пытаясь в чём-то переубедить Дмитрия Юрьевича, не пытаетесь ли Вы «научить учёного», у которого, в отличие от Вас, есть за плечами написанный транслятор с двух языков: PL/1 и ассемблера?

А вы поняли в чем пытаюсь переубедить? Потому что судя по постановке вопроса — совершенно нет. Людей после определенного возраста ни в чём переубедить нельзя. Я просто изложил свое видение вопроса (признаюсь не совсем корректно), человек не понял (что вероятно справедливо). Я пытался объяснить, что я имею ввиду. Это же все написано в теме. Почему бы не прочесть, прежде чем обвинять меня в каких-то там эфемерных целях. Я не сектант и никого не пытаюсь убедить в своих представлениях.

Вы, utkin, пишите программу на своём языке, а Дмитрий Юрьевич — на PL/1. Оценка — по двум старым критериям: скорость и объём занятой памяти.

Во-первых, язык не дописан. Во-вторых, сравнивать компилятор и интерпретатор — это сильно. И Вы ещё мне после этого предъявляете претензии? Удивительно просто.

«лучшее знание архитектуры процессоров и памяти»

Я нисколько не сомневаюсь в его знаниях. Именно поэтому и веду беседу с данным человеком.

✅  2019/10/31 13:11, Александр Коновалов aka Маздайщик          #49 

Никаких «16-байтовых страниц» не существует — это выдуманный utkin’ом термин. Менеджеры памяти (тот же malloc() для Си), как правило, выделяют адреса, кратные 16 байтам.

Это тоже самое и есть :). Минимальная единица в менеджере памяти страница.[Источник?] И она, кстати, не обязана быть равна 16 байтам, просто наиболее часто используется данная величина. И выделяются не адреса, а блоки памяти. Блок всегда имеет целое число страниц. Вот это самое я и писал между прочим. Согласен был не точен в формулировках. Но речь шла об этом. И о том, что в свете такого механизма работы с памятью прямое указание битов не даст Вам никакой эффективности в сравнении с традиционными типами данных.

Давайте будем точными в формулировках.

Страница — блок памяти, с типичным размером 4096 байт (например, на i386 или x64). Используется для организации виртуальной памяти. Например, на 32-разрядной архитектуре процесс имеет 4 Гбайта собственного адресного пространства. Некоторые из страниц этого пространства отображаются на физические страницы ОЗУ компьютера, некоторые могут быть просто не выделены (в свопе или вообще не доступны). Операционная система выдаёт память процессу именно что страницами (смотрим VirtualAlloc() на Windows, mmap() на POSIX).

Рантайм (runtime support library) — библиотека поддержки времени выполнения языка программирования. Реализует логику встроенных средств языка: встроенные функции (если есть), обработка исключений, работа с памятью (включая сборку мусора) и т.д. Рантайм всегда неявно компонуется с программами, написанными на конкретном языке. Экстракоды, которые обсуждались на этом сайте, являются частью рантайма.

Менеджер памяти языка программирования — часть рантайма, которая отвечает за работу с памятью. Он может вызываться либо неявно (динамические массивы в Бейсике/Паскале, оператор new в C++), либо быть обычной функцией библиотеки (malloc() в Си). И он, как правильно заметил utkin, выделяет блоки. Но блоки идентифицируются адресами. Это хорошо видно в Си/Си++ — функция malloc() и оператор new возвращают указатель — адрес начала блока.

Современные архитектуры процессоров требуют, чтобы данные в памяти выравнивались. Т.е. если программист хочет работать с двухбайтовым числом (uint16_t в C99), его адрес должен быть чётен, с четырёхбайтовым — кратен четырём, с восьмибайтовым — кратен восьми.

Некоторые архитектуры (почти все, кроме x86) жёстко требуют выравнивания — обращение (чтение или запись) по не выровненному адресу приводит к аппаратному прерыванию. Архитектура x86 допускает обращение по не выровненному адресу, но выполняется оно медленнее.

Поэтому из соображений выравнивания данных компиляторы вынуждены

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

Есть ещё выравнивание по кэш-линиям. Границы кэш-линий проходят по адресам, кратным их размеру (обычно 16 байт). Объекты желательно размещать так, чтобы они занимали минимум кэш-линий. В этом случае (а) кэш используется эффективнее, (б) улучшается распараллеливаемость программы (сами нагуглите про когерентность кэша в многоядерных системах). Ради этого выравнивания структуры данных желательно размещать по адресам, кратным длине кэш-линии.

Функция malloc() понятия не имеет, для каких структур данных она вызывается, поэтому вынуждена действовать консервативно — выделять память с наибольшим выравниванием. Поэтому адреса, возвращаемые этой функцией, как правило кратны 16 байтам. Но не обязательно аргумент функции malloc() будет округляться до ближайшего числа, кратного 16. Можно придумать реализацию, которая, наоборот, с наименьшими накладными расходами выделяет память блоками по 8, 24, 40, …, 16×n+8 байт, но при этом адреса блоков будут кратны 16 байтам.

Если выделяется массив двухбайтовых слов, то его достаточно выравнивать по чётным адресам. Если выделяется строка символов ASCII или UTF-8, то её можно вообще не выравнивать.

✅  2019/10/31 13:37, Александр Коновалов aka Маздайщик          #50 

И? Это означает, что я не должен касаться этой темы и изучать её?

utkin, изучать, конечно, надо. И Вы пока находитесь в самом начале этого обучения — осваиваете терминологию и имеете общие представления. У меня тоже так когда-то было. У Вас всё впереди.

Рекомендую попрограммировать на Си (не Си++, а Си) — так Вы освоите указатели. А также познакомиться с ассемблером. Программировать на нём не обязательно, достаточно вдумчиво прочитать один или несколько учебников. Я так со многими языками знаком — не программировал, но вдумчиво читал учебники.

Для лучшего понимания работы компьютера рекомендую «Современные операционные системы» Таненбаума. Где прочитать про устройство кучи? Не знаю точно. Я читал когда-то у Кнута, но там сложно. Кратко и поверхностно есть в книге Вирта «Алгоритмы и структуры данных» (мне помнится, что есть, лень скачивать). Ещё есть в книге Дракона.

У Вас всё впереди! Дерзайте!

Ничего хорошего не получится. Из-за жесткой привязки к FPU все переведется в FLOAT(53) и FLOAT(24).

kt, разумно. Мне было интересно — синтаксическая ошибка или расширение до ближайшего доступного. А FLOAT(63) (10-байтовое расширенное) поддерживается?

Похожее я видел, вроде в Аде. В ней, вроде, доступны числа с фиксированной запятой, в объявлении указывается число десятичных цифр после запятой. Компилятор подбирает подходящее число битов для представления дробной части, чтобы все десятичные знаки влезли. Например, для двух знаков он выберет 7 бит дробной части, т.к. 100 < 128.

✅  2019/10/31 13:41, Александр Коновалов aka Маздайщик          #51 

Ещё есть в книге Дракона.))

Не у Паронджанова (не к ночи упомянут), а в книге Ахо Сети Ульмана «Компиляторы: принципы, технологии, инструментарий» с драконом на обложке.

✅  2019/10/31 13:52, Автор сайта          #52 

Знаете, когда-то я читал такое: «Хватит выравнивать на границу слова или двойного слова, это было важно для быстродействия во времена первых Пентиумов, но не сейчас. В современных x86 влияние выравнивания на производительность сильно уменьшена». Но я не пытаюсь кого-то убедить, что это так
  • потому что я не могу сослаться на авторитетный источник,
  • потому что я не изучал это влияние вживую, на реальном оборудовании; у меня нет опыта, у меня нет легко проверяемых доказательств.
Вот поэтому я не настаиваю на таком утверждении. Вы же настаиваете, как будто у Вас и источники есть, и доказательства.

Ну я как бы и собственного вертолета не имею, но это не значит, что я не могу о них говорить.

Ну тогда Вы должны осознать свою роль, как пассажира. Пилоту — пилотово, а пассажиру — пассажирово.

сравнивать компилятор и интерпретатор — это сильно

Я предложил сравнить не интерпретатор с компилятором, а эффективность реализованных с их помощью программ. Чтобы сравнить, чьё выравнивание на границы слов эффективнее. Не на словах, а на деле.

✅  2019/10/31 14:28, kt          #53 

А FLOAT(63) (10-байтовое расширенное) поддерживается?

Нет, и это уже мое решение (в исходном трансляторе были только FLOAT(24)). Я считаю, что 10-байтовый FLOAT не для программистов.
Это решение округления по рецепту: «Как поймать 10 львов?» (поймать 20 и 10 выпустить). Фактически это изнанка вычислительных процессов, которую лучше не видеть, чтобы не сталкиваться с такими понятиями как «дикие ошибки», «циклические дыры», «грязный ноль» и т.п. Конечно, если аккуратно их использовать только для запоминания и восстановления состояния FPU никакой беды не будет, но не все программисты это понимают. Поэтому, на мой взгляд, пусть уж лучше где-то ответ загрубится, из-за промежуточного результата, сохраненного как FLOAT(53), а не «сырого» FLOAT(63), чем случайно получится мешанина из нулей и единиц вместо правильного IEEE 754, которую тяжело заметить.

✅  2019/10/31 16:53, Александр Коновалов aka Маздайщик          #54 

Знаете, когда-то я читал такое: «Хватит выравнивать на границу слова или двойного слова, это было важно для быстродействия во времена первых Пентиумов, но не сейчас. В современных x86 влияние выравнивания на производительность сильно уменьшена». Но я не пытаюсь кого-то убедить, что это так

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

Я сделал замер производительности:

https://mazdaywik.github.io/direct-link/align-benchmark/

В тесте выделяется память на 3 матрицы, первые две инициализируются небольшими числами, в третью записывается произведение трёх матриц. Функция alloc_ali просто вызывает malloc(size), функция alloc_ali возвращает malloc(size+1)+1, т.е. выделяет на один байт больше, чем надо и возвращает указатель, смещённый на один байт.

Компилятор GCC, когда видит память, выделенную malloc(), генерирует более эффективный код. По-видимому, он учитывает тот факт, что диапазоны адресов не перекрываются. Поэтому, чтобы замер производительности был честным, пришлось написать функцию alloc_ali, которая просто является синонимом для malloc’а.

Сделаны замеры для нескольких типов данных: double, float, int, short, char. Для char’а ожидается, что разность будет на уровне погрешности измерений, поскольку для него выравнивание роли не играет.


Тестировал компиляторами GCC 4.8.1 (флаг компиляции -O1) и BCC 5.5.1 (флаг -O) на машине с процессором Intel® Core™ i5-5200U 2,20 ГГц, Windows 10 x64.

Компилятор GCC 4.8.1, 32-разрядный
test double+alloc_ali, checksum 2999.0, ma 00A7F020, mb 01BC6020,
mc 02D1D020, time 14.777000
test double+alloc_una, checksum 2999.0, ma 03E65021, mb 04FA7021,
mc 060F3021, time 15.919000
test float+alloc_ali, checksum 2999.0, ma 0723F020, mb 07AEE020,
mc 0839D020, time 14.243000
test float+alloc_una, checksum 2999.0, ma 08C45021, mb 094EC021,
mc 09D9D021, time 14.761000
test int+alloc_ali, checksum 2999.0, ma 0A64F020, mb 0AEFE020,
mc 0B7AD020, time 9.498000
test int+alloc_una, checksum 2999.0, ma 0C050021, mb 0C8F5021,
mc 0D1A8021, time 10.147000
test short+alloc_ali, checksum 2999.0, ma 0DA51020, mb 0DEA1020,
mc 0E303020, time 8.841000
test short+alloc_una, checksum 2999.0, ma 0E763021, mb 0EBC9021,
mc 0F029021, time 9.221000
test char+alloc_ali, checksum -73.0, ma 0F487020, mb 0F6BC020,
mc 0F8F5020, time 8.588000
test char+alloc_una, checksum -73.0, ma 0FB2D021, mb 0FD61021,
mc 0FF96021, time 8.542000
Компилятор GCC 4.8.1, 64-разрядный
test double+alloc_ali, checksum 2999.0, ma 0000000000AD0040, mb 0000000001C1D040,
mc 0000000002D69040, time 13.336000
test double+alloc_una, checksum 2999.0, ma 0000000003EBD041, mb 0000000005006041,
mc 0000000006155041, time 14.499000
test float+alloc_ali, checksum 2999.0, ma 000000000729F040, mb 0000000007B44040,
mc 00000000083FA040, time 12.770000
test float+alloc_una, checksum 2999.0, ma 0000000008CAC041, mb 000000000955A041,
mc 0000000009E04041, time 13.470000
test int+alloc_ali, checksum 2999.0, ma 000000000A6B2040, mb 000000000AF51040,
mc 000000000B7F0040, time 9.597000
test int+alloc_una, checksum 2999.0, ma 000000000C095041, mb 000000000C93E041,
mc 000000000D1EE041, time 10.047000
test short+alloc_ali, checksum 2999.0, ma 000000000DA9A040, mb 000000000DEF2040,
mc 000000000E356040, time 8.765000
test short+alloc_una, checksum 2999.0, ma 000000000E7BD041, mb 000000000EC1F041,
mc 000000000F070041, time 9.044000
test char+alloc_ali, checksum -73.0, ma 00000000006F7040, mb 000000000F4CC040,
mc 000000000F70B040, time 8.472000
test char+alloc_una, checksum -73.0, ma 000000000F946041, mb 000000000FB77041,
mc 000000000FDA4041, time 8.426000
Компилятор BCC 5.5.1, 32-разрядный
test double+alloc_ali, checksum 2999.0, ma 02560004, mb 036A0004,
mc 047E0004, time 19.562000
test double+alloc_una, checksum 2999.0, ma 05920005, mb 06A60005,
mc 07BA0005, time 19.219000
test float+alloc_ali, checksum 2999.0, ma 08CE0004, mb 09580004,
mc 09E20004, time 15.266000
test float+alloc_una, checksum 2999.0, ma 0A6C0005, mb 0AF60005,
mc 0B800005, time 15.562000
test int+alloc_ali, checksum 2999.0, ma 0C0A0004, mb 0C940004,
mc 0D1E0004, time 14.719000
test int+alloc_una, checksum 2999.0, ma 0DA80005, mb 0E320005,
mc 0EBC0005, time 15.406000
test short+alloc_ali, checksum 2999.0, ma 0F460004, mb 0F8B0004,
mc 0FD00004, time 12.156000
test short+alloc_una, checksum 2999.0, ma 10150005, mb 105A0005,
mc 109F0005, time 12.516000
test char+alloc_ali, checksum -73.0, ma 006C0004, mb 10E40004,
mc 11070004, time 11.328000
test char+alloc_una, checksum -73.0, ma 112A0005, mb 114D0005,
mc 11700005, time 11.422000
Выводы следующие.
  • Вещественная арифметика медленнее целочисленной. Где-то раза в полтора.
  • Разница между 32-разрядным и 64-разрядным кодом (для GCC) небольшая, 64 чуть-чуть быстрее.
  • Разница в производительности double→float и int→short→char, по-видимому, вызвана меньшим размером данных: эффективнее используется кэш, меньше обращений к памяти и т.д.
  • Не выровненный доступ медленнее выровненного, но не сильно.
  • Современный GCC выравнивает указатели по 16 байтам точно, старенький BCC — по 4 байтам.

Если у кого-то есть AMD, либо процессоры Intel других серий (скажем, Atom какой-нибудь), интересно было бы сравнить.

✅  2019/10/31 17:04, kt          #55 

мы немного касались этого вопроса, здесь же, в обсуждении статьи "Идеальный транслятор":

Но вернемся к нашим баранам, т.е. к тесту. Я попытался вручную повторить некоторые приемы оптимизации. Замена деления умножением, к моему удивлению, совершенно ничего не изменила. Похоже, процессор сам оптимизирует случаи вроде деления на степень двойки. А вот выравнивание данных на 16 привело не к ускорению, а к замедлению работы. Выравнивание данных здесь вообще выглядит не очень. Я рассчитываю, что будет использовано 5000х5000х4=100 000 000 байт, а транслятор IBM реально использовал 400 миллионов. Для 32-х разрядной системы это значительная разница. Ожидаемо самый большой вклад времени дает запись в массив (чуть ли не половина времени), что ещё более снижает вклад остальных оптимизаций.

✅  2019/11/01 17:18, Автор сайта          #56 

Теперь у меня есть авторитетный источник :)

Не выровненный доступ медленнее выровненного, но не сильно.

Перевёл Ваши замеры в проценты (сравнивал чётные строки с нечётными), максимальный процент разницы — 8,02. А минимальный — минус 0,54, т. е. выравненный доступ оказался медленнее невыравненного. Вывод: утверждение «невыравненный доступ, в отличие от выровненного, делается за два такта процессора, а не за один, значит он в два раза медленнее» от истины достаточно далеко.

✅  2019/11/01 19:38, kt          #57 

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

✅  2019/11/05 14:11, Александр Коновалов aka Маздайщик          #58 

Автор сайта:

Теперь у меня есть авторитетный источник :)

Я бы не назвал этот замер авторитетным источником. Во-первых, он померен только на одном процессоре. Во-вторых, замер был сделан только один.

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

Правильнее было бы сделать несколько замеров (минимум десяток), усреднить результаты и посчитать доверительный интервал. Тогда процент имеет смысл считать. Сейчас цифра 8,02 % без доверительного интервала ни о чём не говорит. Может быть, её надо округлить до 8,0 %, может быть, до 8 %, может быть до 5 или 10 %.

Если в тестируемой функции память распределять при помощи malloc, то на GCC производительность увеличивается вдвое. Потому что компилятор видит, что три указателя ссылаются на три независимых участка памяти. Благодаря этому компилятор может сгенерировать более эффективный код. Без подобного знания компилятор считает, что запись по любому указателю может повлиять на чтение других указателей — вдруг участки памяти перекрываются. Поэтому потеря производительности из-за невыровненного доступа «тонет» в неэффективном коде.

GCC выравнивает память, судя по адресам, на 16×N байт (32 или 64). BCC выравнивает на 4 байта. Не исключено, что заметная разница для GCC и малозаметная для BCC вызвана именно изначальным выравниванием.


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


У меня был опыт попытки оптимизации структур данных для своего компилятора Рефала.

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

Сначала звенья этого списка у меня состояли из 5 слов (5×4 байт на 32-разрядной машине и 5×8 на 64-разрядной): указатель налево, указатель направо, поле тега (хотя для него достаточно байта, оно из-за выравнивания занимает слово) и два слова на значение. Для большинства тегов использовалось только одно слово из двух, только для ссылок на функции использовалось два: указатель на функцию и указатель на имя (const char*).

Ссылки на функции я переделал: вместо двух указателей имеется теперь указатель на дескриптор, который содержит указатель на функцию и указатель на имя.

Казалось бы, размер узла теперь стал равен 4 словам: 16 или 32 байта соответственно. Должно было лучше влезать в кэш и всё такое. Платой за это был косвенный доступ к функции, требующий дополнительного разыменования. Вот. Никакой измеримой разницы в производительности я не увидел. Никакой. Измеримой. Разницы в производительности. А я её мерял, тщательно, до и после.

Кстати, проблема может быть в BCC. Он выравнивает выделяемую память на 4 байта, поэтому никакого фактического выравнивания могло и не быть. Только сейчас подумал.

✅  2019/11/07 10:58, kt          #59 

В качестве маленького штриха отмечу, что «куча» в PL/1-KT организована почти так же: указатель направо, указатель налево, тэг и сам выделенный элемент. Единственный тэг показывает, занят или свободен данный элемент «кучи» и этот единственный бит спрятан прямо в указателе.
Таким образом, можно выделить элемент любого размера, хоть в байт, но адрес обязательно будет выравнен на 2, поскольку при использовании адреса из «кучи» в указателях этот бит всегда гасится.

✅  2021/01/16 00:00, alextretyak          #60 

На первый взгляд эта тема действительно очень актуальна и пример с устаревшим самолётом звучит крайне убедительно (можно ещё добавить к ‘последние достижения науки и техники’ в скобочках: интернет на борту и большой LCD тач экран на спинке каждого кресла самолёта). Но. Если погрузиться в детали, то не всё так однозначно/просто. Разберу конкретные признаки, приведённые в статье:

оператор «goto»;

В целом согласен, т.к. достаточно break/continue с меткой, а также конструкции defer (как в Go и Swift). Все остальные варианты использования goto порождают спагетти-код.

такая обработка исключений, которая ещё хуже «goto», когда исключение неизвестно где возникает и неизвестно куда передаёт управление;

Очень больная/большая тема.
По вопросу исключений я и сам менял свою позицию, но решить этот вопрос одними рассуждениями — невозможно. Требуется проанализировать много-много реальных примеров, практического кода, самого разного, на разных языках программирования, с разными способами обработки ошибок, чтобы хоть как-то приблизиться к окончательному решению по данному вопросу. Лично я пока остановился на «устаревшей» модели, принятой в Python и C++, так как ничего реально лучшего (не ‘на словах’/‘в теории’ (см. моё предложение), а на практике) я посоветовать не могу.

постфиксные операции «++» и «--», которые для большинства — загадочны;

Ещё один спорный момент. В Swift 3 вообще убрали «++» и «--». Но если я правильно понимаю автора, то против префиксных «++» и «--» он ничего не имеет? Тогда готов согласиться, хотя и считаю запись `i++` красивее `++i`.

приведения типов, влекущие «тайное» изменения значения;

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

нулевой указатель

Также непонятно, о чём речь. Автор против nullable-типов? Или имеется в виду решение этой проблемы в стиле Kotlin (https://kotlinlang.ru/docs/reference/null-safety.html), с обязательной проверкой на null?

возможность присвоить неинициализированное значение

А что плохого, если язык поддерживает явное указание создания неинициализированной переменной, как например D
int x = void;
Это может быть полезно для создания очень больших массивов, в которых реально использоваться будет лишь небольшая часть элементов.

визуальный мусор типа «begin» и «end», особенно ЗАГЛАВНЫМИ буквами;

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

контроля возможного переполнения при арифметических операциях;

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

приоритетов операций а-ля Lisp или Forth;
контроля границ массивов а-ля Си;
возможности вернуть из функций объекты как скалярные, так и не скалярные, как фиксированного, так и переменного размера;
функций — объектов первого класса;
оператора «for each»;
вывода типов;

Согласен.

обращения по абсолютным адресам
синтаксис, прогибающий под себя программистов, а-ля Forth;
программирования в стиле доказательств;
зрительных ориентиров в тексте, позволяющих отличить операции от операндов;

Не совсем понял, о чём это.

✅  2021/01/16 13:37, Автор сайта          #61 

Очень больная/большая тема… По вопросу исключений ... я пока остановился на «устаревшей» модели, принятой в Python и C++…

Была статья «Обработка ошибок», которая вызвала большое число откликов. Но Вы не отметились в дискуссиях по теме. Вероятно, не заметили статью.

то против префиксных «++» и «--» он ничего не имеет?

Я протестую не против синтаксиса — префиксного или постфиксного, а против семантики постфиксных «++» и «--».

о каком приведении типов идёт речь

Например, присвоение переменной типа int значения типа long, float или double часто делается с искажением значения. В таких случаях надо либо явно «убивать» лишние биты значения, либо должно быть реагирование на ошибку, если что-то теряем в лишних битах:
char a = выбрать 8 младших битов (9876543210);	// явное обрезание лишних битов
char b = 9876543210; // неявная потеря битов. Если потеря и вправду
// случается, возбуждаем исключение
А вот ещё примерчик на похожую тему:
#include <stdlib.h>
main () {
double x = 0.1;
if (x*6 == 0.6)
printf("Здесь всё прозрачно.\n");
else
printf("Что-то пошло не так, тут поработали тёмные силы.\n");
return;
}

Автор против nullable-типов?

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

А что плохого, если язык поддерживает явное указание создания неинициализированной переменной… Это может быть полезно для создания очень больших массивов, в которых реально использоваться будет лишь небольшая часть элементов.

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

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

Биты переполнения и так устанавливаются, хотим мы этого или нет. А вот условный переход по условию «переполнение» замедляет код не в разы, а лишь на проценты — даже не на десятки процентов. Дмитрий Караваев на этом сайте озвучивал цифры на сей счёт.

Не совсем понял, о чём это

обращения по абсолютным адресам

int* ptr = (int*) 1234;	// запросто получаем абсолютный адрес
*ptr = anything; // и пишем по нему
Неужели это нормально?

синтаксис, прогибающий под себя программистов, а-ля Forth

В Форте все слова должны быть разделены пробелами, нельзя написать, например
A+B-C
Обязательно
A + B — C
А постфиксная запись? На неё мозги надо особо настраивать.

зрительных ориентиров в тексте, позволяющих отличить операции от операндов

Форт вполне допускает такую запись:
  blabla_1 blabla_2 blabla_3 
Но что тут функция, а что операнд? Сколько было операндов в стеке до слова «blabla_2» и сколько после? Вы этого не узнаете, пока не заглянете в описание каждого слова.

✅  2021/01/19 00:00, alextretyak          #62 

Я протестую не против синтаксиса — префиксного или постфиксного, а против семантики постфиксных «++» и «--».

Здорово, что протестуете :)(:, но что конкретно вы предлагаете?
1. чтобы `i++` работало точно также как и `++i`;
2. запретить `i++`, оставив только `++i`;
3. разрешить `++i`, `i++` и `arr[++i]`, но запретить `arr[i++]`, что мне больше всего нравится.

Например, присвоение переменной типа int значения типа long, float или double часто делается с искажением значения. В таких случаях надо либо явно «убивать» лишние биты значения, либо должно быть реагирование на ошибку

С этим согласен.

Да. Адрес, указывающий незаконный участок памяти, сам незаконен.

И какие альтернативы? Давайте разбирать конкретные примеры.

Вот в 11l есть тип `Словарь`\`Dict` (в C++ это `std::map`). У этого типа есть метод `find()`, который по заданному ключу возвращает либо N/null\Н/нуль (если такого ключа в словаре нет), либо соответствующее значение. (Т.е. метод `find()` возвращает Nullable-тип `ValueType?`.) Но чтобы использовать значение, которое вернул `find()`, необходимо либо явно проверить его на N/null\Н/нуль, либо использовать оператор `?` (например так: `dict.find(key) ? default_value`). Как вы предлагаете изменить это? Какая сигнатура и тип возвращаемого значения должны быть у метода `find()`?

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

Не могли бы вы указать, где именно это описано в статье «Обработка ошибок».

Биты переполнения и так устанавливаются, хотим мы этого или нет.

Да, но проверка этих битов далеко не бесплатна. На современных суперскалярных процессорах условные переходы стоят дорого и, вероятно, со временем будут становиться ещё дороже.

Здесь есть ссылка на обсуждение (https://news.ycombinator.com/item?id=11595398), в котором говорится о двукратном падении производительности:
... a 2x slowdown, which isn't acceptable if Rust wants to compete with C++.

А вот условный переход по условию «переполнение» замедляет код не в разы, а лишь на проценты — даже не на десятки процентов. Дмитрий Караваев на этом сайте озвучивал цифры на сей счёт.

Откуда эти результаты? Можно исходный код тестирующей программы, с помощью которой получены эти данные? Хотя, и самих конкретных данных в цифрах я не вижу в самой статье (хотя они есть в комментариях и противоречат высказыванию «замедляет код лишь на проценты»).

В случае озвученного мной выше замедления в 2 раза, источник (http://danluu.com/integer-overflow/) приводит такой код:
Без проверки на переполнение:
...
add %esi, %edi
...
С проверкой на переполнение:
...
add %esi, %edi
jo <handle_overflow>
...
Из того же источника:
Для компрессии bzip2 падение производительности (при включении контроля целочисленного переполнения) составляет 28%.
А код:
for (int j = 0; j < n; ++j) {
sum += a[j];
}
выполняется в 6 раз медленнее при включении контроля переполнения!

✅  2021/01/20 00:10, Автор сайта          #63 

Но чтобы использовать значение, которое вернул `find()`, необходимо либо явно проверить его на N/null\Н/нуль, либо использовать оператор `?` (например так: `dict.find(key) ? default_value`). Как вы предлагаете изменить это? Какая сигнатура и тип возвращаемого значения должны быть у метода `find()`?

Тип возвращаемого значения остаётся прежний. Просто find() помечается как ненадёжная функция, внутри неё есть ветка, возвращая ошибку (а не Null). Снаружи функция обязана быть проверена на ошибку. В статье «Обработка ошибок» это разобрано.

Не могли бы вы указать, где именно это описано в статье «Обработка ошибок».

Разделы «Обработка ошибок малой кровью» и «Реализация с двумя адресами возврата».

Да, но проверка этих битов далеко не бесплатна. На современных суперскалярных процессорах условные переходы стоят дорого и, вероятно, со временем будут становиться ещё дороже.

Если Вы не собираетесь писать низкокачественное ПО, то Вы будете проверять и указатели на равенство Null, и результаты вычислений на переполнение. Нельзя же игнорировать ошибки в программе. Те же переполнения если возникают, то на это есть причина. Я же предложил такие способы проверки ошибок, которые с точки зрения процессора не будут стоить ничего в сравнении с тем, как если бы все проверки делали «по старинке», условными операторами. Но синтаксически выгоднее, т. к. код становится компактнее.

говорится о двукратном падении производительности:... a 2x slowdown, which isn't acceptable if Rust wants to compete with C++.

Так в Rust объекты типа Option и Result (которыми проверяют правильность результатов) не помещаются в регистр. При возврате значений этого типа результат будет возвращён не в регистре, а записан в память. А дальше — как повезёт. Если объект хранится в кэше, то повезло. А если нет, то на обращение к памяти уходит до 100-150 тактов. Есть хорошая статья на эту тему: «Стоимость операций».

Можно исходный код тестирующей программы, с помощью которой получены эти данные?

Эти данные получены от Д.Ю. Караваева, разработчика отечественного компилятора PL/1. Возможно, он прочитает эти строки и ответит Вам.

замедления в 2 раза… 28%… в 6 раз медленнее…

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

Кстати, в языке D ветры дуют в ту же сторону — в сторону повышения надёжности. Аспектно-ориентированное программирование, контракты. В языке D контакты проверяют как правильность входных данных, так и выходных. Поддержка на уровне синтаксиса.

А что касается приведённых Вами цифр — с трудом в них верится. Ведь переход по условию переполнения будет весьма редок. Ведь именно такой переход будет вызывать торможение (10-20 тактов для 'wrong branch of if', если верить статье «Стоимость операций». А когда нет перехода, то процессор быстро проскочит на следующую команду.

✅  2021/01/23 00:35, Автор сайта          #64 

но что конкретно вы предлагаете?
1. чтобы `i++` работало точно также как и `++i`;
2. запретить `i++`, оставив только `++i`;
3. разрешить `++i`, `i++` и `arr[++i]`, но запретить `arr[i++]`, что мне больше всего нравится.

Забыл ответить. Я был бы не против, если эти постфиксные операции работали абсолютно одинаково с префиксными, делали бы одно и то же. Но это бы внесло путаницу, ибо все привыкли к другой семантике постфиксных операций. Поэтому от них проще отказаться. Хотя местами это удлиняет код.

✅  2023/03/27 10:49, ChatGPT отвечает          #65 

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

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

✅  2023/03/29 22:25, move.l -(*void),(a0)+          #66 

но что конкретно вы предлагаете?
1. чтобы `i++` работало точно также как и `++i`;
2. запретить `i++`, оставив только `++i`;
3. разрешить `++i`, `i++` и `arr[++i]`, но запретить `arr[i++]`, что мне больше всего нравится.

очередные упрощенцы имени графа Льва Толстого. А ведь, в процессоре Motorola MC 68000 было разных 14 режимов адресации — и никто не жужжал по поводу сложности. Были, например, такие инструкции как
;псевдокод из мануала 68kprm.pdf, section 2 addressing capabilities 
; и далее, описание опкодов команд
movem <list>, <ea> ; список регистров -> эффективный адрес
; или
movep Dx,(d16,Ay) ;
move16 (Ax)+,(Ay)+
move <ea>,<ea> ; из эффективного адреса в эффективный адрес слева направо
; например
move.l (a0)+,-(a1);
; псевдокод
; move.l ([bd,An,Xn],od),(d8,An,Xn) ;MC68020,MC68030,MC68040

; про структуру опкодов

; table 2-1. instruction word format definitions
; mode, register, D/A, W/L, scale*1,*2,*4,*8, BS(base register added/suppress),
; bd size=base displacement size(reserved, null, word, long),
; I/IS index/indirect selection indirect and indexing operand determined
; in conjunction with bit 6, index suppress

; table 2-2. IS-I/IS memory indirect action encodings
; IS 0 index/indirect 000 No Memory Indirect Action
; IS 0 I/IS 001 Indirect Preindexed with Null Outer Dispacement
; IS 0 I/IS 010 Indirect Preindexed with Word Outer Dispacement
; IS 0 I/IS 011 Indirect Preindexed with Long Outer Dispacement
; IS 0 I/IS 100 Reserved
; IS 0 I/IS 101 Indirect Postindexed with Null Outer Dispacement
; IS 0 I/IS 110 Indirect Postindexed with Word Outer Dispacement
; IS 0 I/IS 111 Indirect Postindexed with Word Outer Dispacement
; IS 1 I/IS 000 No Memory Indirect Action
; IS 1 I/IS 001 Memory Indirect with Null Outer Displacement
; IS 1 I/IS 010 Memory Indirect with Word Outer Displacement
; IS 1 I/IS 011 Memory Indirect with Long Outer Displacement
; IS 1 I/IS 100-111 Reserved

;далее в мануале идёт раздел 2.2 EFFECTIVE ADDRESSING MODES
; 2.2.1 Data Register Direct Mode
; 2.2.2 Address Register Direct Mode
; 2.2.3 Address Register Indirect Mode
; 2.2.4 Address Register with Postincrement Mode
; 2.2.5 Address Register with Predecrement Mode
; 2.2.6 Address Register Indirect with Displacement Mode
; 2.2.7 Address Register Indirect with Index (8-bit Displacement) Mode
; 2.2.8 Address Register Indirect with Index (Base Displacement) Mode
; 2.2.9 Memory Indirect Postindexed mode
; 2.2.10 Memory Indirect Preindexed mode
; 2.2.11 Program Counter Indirect with Displacement Mode
; 2.2.12 Program Counter Indirect with Index (8-bit Displacement) Mode
; 2.2.13 Program Counter Indirect with Index (Base Displacement) Mode
; 2.2.14 Program Counter Memory Indirect Postindexed Mode
; 2.2.15 Program Counter Memory Indirect Preindexed Mode
; 2.2.16 Absolute Short Addresing Mode
; 2.2.17 Absolute Long Addresing Mode
; 2.2.18 Immediate Data
Вот видите: в мануале перечислено даже 18 режимов эффективного адреса. При этом было 16 32-разрядных регистров: 8 адресных A0..A7 и 8 данных D0..D7. Самой же главной "фишкой" процессора Моторолы 68к было, на мой взгляд, ортогональное использование регистров: в любой команде можно было использовать любой регистр в любом режиме адресации и вычисления эффективных адресов. Но да, это классический CISC. Это "сложна".

Хотя, если быть точным — мне кажется, что эти предекременты/постинкременты тянутся ещё из PDP,
у которого хоть было 8 регистров более простых, аналог
move.l -(a0),(a1)+
тоже можно было изобразить одной командой. Естественно, что Си как "высокоуровеневый ассемблер" повторял этот набор команд, опкодов процессора буквально. Отсюда и тянутся все эти предекременты и постинкременты — ещё из набора команд этих CISC CPU: PDP8, MC68k (в MC68020+ вон видите выше — наворотили целых 18 режимов адресации, вычисления EA; к тому же, запись вида ([bd,An,Xn],od) по сути означает дважды косвенную адресацию
(привет, (*this)->VirtualMethodTable(...), разыменовывающий (*void)(...)) 

✅  2023/03/29 22:29, void          #67 

Мне более интересно другое. Откуда же ноги растут у этого вот упрощения, усреднения, мифологизации "среднего человека", мейстрима, упрощения — равнения по отстающим же??? Наткнулся на интересную книгу, читаю сейчас: Роуз Тодд. "Долой среднее!"Процитирую бредисловие:

Критика усредняющего разума

Книга неутомимого ниспровергателя стереотипов мышления, сковывающих познание изменяющегося человека в изменяющемся мире, выпускника Высшей школы образования Гарвардского университета Тодда Роуза по праву может быть определена ёмким символом «Манифест индивидуальности». Смысло­образующий мотив этой книги, названной The End of Average (почти в буквальном переводе — «Конец усредненности»), — бунт против всяческого усреднения и диктатуры стандарта, технологий обезличивания, обесценивающих неповто­римость каждого из нас, низводя её до уровня погрешности, отклонения от Нормы. Рискуя впасть в преувеличение, отмечу, что по своему значению и роли в смене ценностных установок это полемическое произведение вполне может конкурировать с эпохальными трактатами создателя философии нестабильности, лауреата Нобелевской премии Ильи Пригожина «Конец определенности: Время, Хаос и новые законы природы» (2000) и возмутителя спокойствия, социального философа и политолога Фрэнсиса Фукуямы «Конец истории и последний человек» (1992).

Каковы исторические и научные истоки мифологии «среднего человека», «среднего социального класса», «среднего ученика», «нормальной» профессиональной карьеры? Как в лоне статистики родились представления о стандарте и Норме, на долгие годы ставшие идеалами познания? Почему вслед за знаменитым бельгийским астрономом конца ХIХ века Адольфом Кетле, разрабатывавшим «метод усреднения», учение о «средних величинах», «типах» и «среднем человеке», идущие по его стопам поколения ученых, подобно язычникам, стали поклоняться таким идеалам, как Стандарт и Норма, а любые отклонения от Стандарта, в том числе индивидуальные различия между людьми, интерпретировать как погрешности измерений, досадные случайности, которые ради точности измерений необходимо отбрасывать, приносить в жертву системе? Как возникло мировоззрение, в котором Стандарты и Нормы, символы статистических усреднений, стали, по сути, идеалами управления на самых различных фабриках, в том числе и на фабрике образования как производстве «среднего ученика» или идеологической фабрике как производстве «среднего человека»?

Тодд Роуз шаг за шагом показывает, что за формулой «Система — всё, Индивидуальность — ничто» проступает явная или неявная вера в то, что обезличивает статистика обширных массивов данных позволит отсеять все лишнее, случайное и найти тот или иной «идеальный тип» — среднестатистическую норму, например, красоты, строения тела или же типовых параметров кабины самолета и тому подобного, под которую должны быть подстроены любые произведения нашего разума.

Переводя эти установки усредняющего разума на язык психологии обыденной жизни, так и хочется сказать: будь в образовании, политике, на производстве ближе к Норме, Стандарту, будь проще — и народ к тебе потянется. А если отклоняешься от Стандарта, заданного алгоритма, типовой программы образования — пеняй на себя. Неслучайно суть усредняющего разума Тодд Роуз иллюстрирует словами признанного классика конвейерных технологий в различных сферах индустриального общества Фредерика Тейлора.

«Готовность прощать самодеятельность — камень, о который споткнулись самые светлые умы, — предупреждал он в статье 1918 года. — Средний человек ни в коем случае не имеет права создавать новую машину, или метод, или процесс в целях замены уже имеющегося варианта».

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

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

Поэтому, когда руководители служб управления персоналом таких крупных компаний, как Google или Microsoft, сетуют на то, что годы, потраченные на поиск и отбор молодых талантов с помощью традиционных психотехнологий тестирования интеллекта или черт личности, стали для них потерянными десятилетиями, винить за это нужно не столько традиционные техники тестирования, не узревшие невидимые таланты, сколько службы подбора персонала, ставшие носителями усредняющего разума и возведшие стандартные тесты, например тесты интеллекта, в ранг сакральных идолов стандар­тизации.

Я пишу об этой опасности, так как новая вера в ХХI веке во всесилие «Больших Данных», которые позволят понять все на свете, даже предсказать непредсказуемое поведение индивидуальности, «вычислить», например, установки потребительского поведения каждого человека, чем-то сродни вере во всесилие усредняющего разума, прокладывающего единственную для всего человечества дорогу технологического прогресса. Неслучайно ещё на границе XIX и XX веков великий мыслитель Владимир Иванович Вернадский предупреждал, что, как только в нашем мышлении мы переходим от понимания каждого человека как индиви­дуальности к безликой категории «люди», мы рискуем потерять самого человека, его уникальность.

Книга Тодда Роуза, на мой взгляд, являет собой подлинный гимн разнообразию жизни, уникальности и ценности каждого человека. Уже много лет, читая студентам факультета психологии МГУ курс «Психология личности», я не устаю повторять «Индивидом рождаются! Личностью становятся. Индивидуальность отстаивают!»

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

И наконец, эта книга с подзаголовком «Манифест индивидуальности» — редкий шанс для смены парадигмы школьного и высшего образования, перехода от парадигмы усредняющего века к парадигме века индиви­дуальности. В веке индивидуальности рано или поздно останется в прошлом представление о «нормальном» жизненном пути как единственно правильном пути с усредненными вехами образования и профессионального роста — пути, который расписан и предопределен, по образному выражению Тодда Роуза, «как по нотам». Например, вместо стандартной образовательной карьеры в стиле Тейлора — школьник, студент, аспирант, доцент, профессор, заведующий кафедрой… — предлагается близкий моему сердцу принцип множественности путей, который составляет ядро развиваемого в России вариативного образования как расширения возможностей для развития личности и компетентного выбора собственной биографии, собственной судьбы.

Будут ли и бизнес, и образование жить в веке индиви­дуальности? Сумеем ли мы вырваться из ловушек усредненного мышления в школе, университете и обществе? Как с иронией говорят, прогнозировать всегда трудно, особенно прогнозировать будущее. Но в том, что книга Тодда Роуза станет мощным подспорьем для любого отважного человека, ищущего свою дорогу в жизни, независимо от чина, звания и профессии, я ни на минуту не сомневаюсь.

Любопытная книжка — любопытные идеи высказаны.

✅  2023/03/30 22:51, Автор сайта          #68 

в процессоре Motorola MC 68000 было разных 14 режимов адресации — и никто не жужжал по поводу сложности.

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

Это "сложна"

Это, в первую очередь, низкопроизводительно с точки зрения расходования такого ресурса, как труд программиста. Много ли сейчас программируют на ассемблере? В исключительных случаях. Ну или «по приколу», как в случае KolibriOS.

предекременты/постинкременты тянутся ещё из PDP

Инкремент и декремент ведут свою историю, как мне кажется, с PDP. В Си они попали оттуда, ибо Си был впервые реализован на PDP.

Си как "высокоуровеневый ассемблер" повторял этот набор команд, опкодов процессора буквально.

Нет, не буквально! В PDP машинные операции инкремента и декремента соответствовали исключительно префиксным операциям Си. Никаких постфиксных операций inc/dec в ассемблере PDP быть просто не могло.

Протест против постфиксных операций связан с туманной и неоднозначной семантикой этих операций. Вот примеры из Википедии, статья «Синтаксис и семантика языка Си»:
int i = 1;    // Описатель - первая точка следования, полное выражение - вторая
i += ++i + 1; // Полное выражение - третья точка следования
printf("%d\n", i); // Может быть выведено как 4, так и 5
i = i++ + 1; // неопределённое поведение
i = ++i + 1; // тоже неопределённое поведение

printf("%d, %d\n", --i, ++i); // неопределённое поведение
printf("%d, %d\n", ++i, ++i); // тоже неопределённое поведение

a[i] = i++; // неопределённое поведение
a[i++] = i; // тоже неопределённое поведение
Не кажется ли Вам, что слишком много проблем для такого малозначительного оператора? И этот абсурд поддерживается компиляторами в 21 веке!

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

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

Языков программирования, наверное, уже около 10 тысяч — выбрать есть из чего. Среди них можно найти языки для классов коррекции (Бейсик, наверное). Примеры обычных языков сами можете подобрать. Ну а для продвинутых есть Haskell и ему подобные.

✅  2023/04/01 03:44, void          #69 

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

Нет, потому что Мотороллеры 68k были довольно высокоуровневой процессорной архитектурой — для "всего лишь" машинного языка. И при этом код оставался всё ещё наглядным и легко читаемым для "просто" ассемблера. MC68060, например, по производительности был почти сравним с Pentium. Дальше — больше. Какое-то время прогресса в этой ISA архитектуре 68k не было, и они ударились в embedded — Coldfire и CPU32. Потом, как глоток свежего воздуха — FPGA переосмысление: ядро MC 68080 и apollo-core, собранный apollo-computer. Там, в 68080 — уже 32 регистра данных и адреса, 64-битное деление и умножение, плюс AMMX аналогичное MMX только 64-битное (из-за которого и показывают прирост в 5-6 раз, да и это не предел, если сделают 128,256,512 и т.п.).

Вот Вам, к примеру, исходники на ассемблере: http://www.apollo-core.com/index.htm?page=coding&tl=3, рядом (и на apollo-computer) есть ролики на ютубчике с вот такими примерами: http://www.apollo-core.com/index.htm?page=coding. В общем, по настоящему — создано для людей: http://apollo-computer.com/createdforcoders.php

Coder-friendly

The goal of the 680x0 was to develop an easy to program coder-friendly CPU. The 680x0 is so easy to code because it does a lot of work to help you out. We can compare 680x0 to a car that is easy to drive and has many driver assistance features like automatic transmission, ABS, ESP, and Tesla autopilot.

A non-coder-friendly CPU makes coding harder, and you lose track of functions. That increases the risk of coding errors. It is much easier to make a stable program on 680x0 than other architectures.

Easy Coding. We truly believe that coding for the 68k is very easy, especially that 68k assembly is really easy to learn. <..>

Что ещё впечатляет: у них есть сравнения производительности http://www.apollo-core.com/index.htm?page=performance Несмотря на то, что этот 68080 реализован на FGPA — тесты Dhrystone и Whetstone показывают для FPGA 93 Mhz производительность на уровне PowerPC 800Mhz. Можно также примерно проэкстраполировать 68060 раз в 5-6, и получить нечто аналогичное Pentium I MMX 1200 Mhz. В общем, есть ещё прогресс в микроархитектурах — только в газетах и по телевизору, то есть — в мейнстриме про это почему-то всем подряд не рассказывают.

Можете долго мне рассказывать про "признаки устаревшей ISA CISC микроархитектуры" в машинном языке MC68k, да и вообще "виртуальность" этого 68080. Но что я вижу, по тестам — будучи реализованном не на довольно мощном FPGA, а например, на заказном ASIC эта микроархитектура скорее всего бы, показывала бы ещё более впечатляющие результаты по производительности.

И не только отдельно CPU, но и всей платформы в целом — см. последний тест всего чипсета, производительность DMA. Довольно впечатляющий прогресс — для этой всей платформы ретрокомпьютерной, в целом. AMIGA FOREVER!!!

А так, он тут маленький такой — потому что ему вырасти не дают (как в том анекдоте). Был бы это не FPGA довольно из дорогих за 700 евро в сборе, а массовый ASIC CPU — и себестоимость, и конечная цена в массовом производстве — тоже была бы гораздо ниже этих 700 евро. Впрочем, не надо верить мне на слово. Поставьте сами какой-нибудь Easy68k и потыкайте примеры веточкой, там занятно — хоть и 68000, а не грядущий 68080 (который кстати Open Source Hardware: там где-то в репозитории исходники на верилоге были; Apollo OS тоже адаптация под опенсорсной AROS). Там по настоящему Ассемблер с человеческим лицом. Не такой уж и низкоуровневый.

Это, в первую очередь, низкопроизводительно с точки зрения расходования такого ресурса, как труд программиста. Много ли сейчас программируют на ассемблере? В исключительных случаях. Ну или «по приколу», как в случае KolibriOS.

Результатом экономии труда времени программиста стало катастрофическое падение его качества. Сейчас запросто накидают какой-нибудь Electron.js с GPU ускорением и Фреймворками на JavaScript — там, где требуется простое окошко с кнопкой. Как работает вся эта машинерия на JS и не нужном там браузерном движке — конечно же, не понимает уже никто. Это же становится уже просто ритуалом каким-то, "среднего" (и весьма) программиста. Халява не даётся бесплатно. Она обходится ещё дороже — для тех, кто вынужден этим поделием пользоваться.

А вот ассемблер — не врёт, и не добавляет "отсебятины". Он наиболее близок к этой мантре из С++ "Zero-cost abstraction". Просто ассемблер — ассемблеру рознь, и "не модно" на нём стало программировать в периоды бурного расцвета RISC и MMX, SSE инструкций — и всё увеличивающейся сложности конпеляторов и кодогенераторов. В мотороллерах (и том же PDP) это было действительно не сложно, и всё ещё понятно и достаточно высокоуровнево. И да — это не реклама. Это просто (дескать, "сложна" и "ненужна") опровергающий контрпример, в который можно ткнуть пальцем, первый пришедший в голову. Просто существует другой путь действительно развития. Да и он всегда тут был, рядышком — просто его перестали замечать, когда это стало вдруг внезапно "не модным".

Нет, не буквально! В PDP машинные операции инкремента и декремента соответствовали исключительно префиксным операциям Си. Никаких постфиксных операций inc/dec в ассемблере PDP быть просто не могло.

Ну, значит в мотороллерах они уже появились. 68k немного напоминает PDP в том смысле, что регистр стека — это простой обычный регистр: R6 в PDP11 или A7 в 68k. А вот они и в PDP11 — извиняюсь, запамятовал — не PDP8: https:// шen.wikibooks.org/wiki/ Software_Engineers_Handbook/ Language_Dictionary/ DEC_PDP-11#Registers
    mov (PC)+, R6 ; SP = (*void++)
void: nop
mov -(PC), -(PC) ; PC = R7, SP = R6

Не кажется ли Вам, что слишком много проблем для такого малозначительного оператора? И этот абсурд поддерживается компиляторами в 21 веке!

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

Опять же, одно дело если читать в исходниках компилятора какого-нибудь нелепого и громоздкого, воистину старорежимного и устаревшего С++ (иначе, откуда у него столько более "эргономичных" форков? Java, D, C#, и т.п.) — какой-нибудь сложный RISC кодогенератор. И совсем другое — наглядные исходники Оберон-2 с кодогенерацией под простой и понятный 68k, например.

Когда же это "среднее усредняющим разумом"-ориентированное на RISC, MMX, SSE сложности внезапно стало мейнстримом, и про то, что на ассемблере может код быть достаточно высокоуровневым и при этом всё легко читаемым (если ISA микроархитектура CPU адекватна и с "человеческим лицом", то есть, coder-friendly — все внезапно позабыли? И стали записывать CISC и вообще ассемблер в "устаревший язык"?

✅  2023/04/01 11:21, MihalNik          #70 

Результатом экономии труда времени программиста стало катастрофическое падение его качества.

Это миф.

наглядные исходники Оберон-2 с кодогенерацией под простой и понятный 68k, например

Качество швейцарских исходников Оберонов да и самого языка довольно низкое.

что на ассемблере может код быть достаточно высокоуровневым и при этом всё легко читаемым (если ISA микроархитектура CPU адекватна и с "человеческим лицом", то есть, coder-friendly

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

✅  2023/04/01 13:50, Автор сайта          #71 

      move.l  #FX+64,$DFF410        ; Set 32bit Ptr (skipp AIFF 64byte header)
move.l #(FX_e-FX-64)/2,$DFF414 ; Set 32bit Length (length in pairs of sample)
move.w #$00FF,$DFF418 ; Play sample on right speaker
move.w #80,$DFF41C ; Set Samplerate 44khz
move.w #01,$DFF41A ; Set 8Bit One-Shot mode
move.w #$8202,$DFF096 ; Enable DMA
Много ли поучительного может найти для себя язык высокого уровня их этого примера (apollo-core.com)? Ассемблер как ассемблер. Он и в Африке ассемблер.

Можно также примерно проэкстраполировать 68060 раз в 5-6, и получить нечто аналогичное Pentium I MMX 1200 Mhz.

Покажите мне вживую хоть один такой процессор в продаже — и я буду стараться сделать его одной из целевых платформ. Вы подсказываете одно абсурдное направление движения за другим. То литературное программирование Вы предлагаете, то в какой-нибудь Лисп Вас заносит, а теперь надо кинуться смотреть на богом забытый ассемблер Моторола. С одной стороны, Ваша информация поразительна объёмом и широчайшим охватом, с другой стороны, в упор не видно какого-то стержня, у Вашего корабля отсутствует компас. Сегодня у Вас ветер сюда дует, а завтра туда, и Вас несёт этим ветром непонятно куда. Какова Ваша цель? Где точка, к которой Вы стремитесь?

"сложна" и "ненужна"

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

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

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

Вы впустую затрагиваете тему CISC и RISC. Тому же Си уже полвека, но и он не соизволит «снизойти» до столь низкого машинного уровня, до таких мелких подробностей. Хотя многие относят его не к языкам высокого уровня, а среднего.

стали записывать CISC и вообще ассемблер в "устаревший язык"?

Не устаревший, а нишевый. Необходимый при написании кодогенераторов и операционных систем (местами). В остальном можно обойтись.

✅  2023/04/01 16:24, Ильдар          #72 

Никаких постфиксных операций inc/dec в ассемблере PDP быть просто не могло.
Ну, значит в мотороллерах они уже появились.

Ахинея. Постфиксные инкремент или декремент вычисляются после какого-либо выражения, например:
*dst++ = *src++;
что равносильно
 *dst = *src;	// само выражение
dst++; src++; // инкремент выполняется ПОСЛЕ!
Но это в Си. А вот у машинных кодов нет выражений! Они выполняются сразу же.

✅  2023/04/02 15:43, Comdiv          #73 

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

Приведённые человеком числа вполне реалистичны и в них нет противоречия с теми числами, что приводит Дмитрий Юрьевич, просто они отображают разницу между оптимизирующими и простыми компиляторами. Для последних, действительно, нет большой разницы от проверок на фоне сравнительно неэффективного основного кода. Для оптимизирующих же драматичная разница может возникать из-за того, что многие эффективные преобразования не могут быть осуществлены при необходимости учёта переполнения, которого могло и не быть. Это тоже не трагично и есть возможность улучшения, но нужно понимать, что делать, а не сравнивать тёплое с мягким.

✅  2023/04/02 20:46, Автор сайта          #74 

А хорошо ли оптимизирует Visual C++? Мне кажется, в его качестве мало кто сомневается. Тем не менее компилятор Дмитрия Юрьевича проигрывает ему весьма немного. А на каких тестах и на сколько процентов — об этом лучше расскажет он сам. Я же только отмечу, что Дмитрий Юрьевич работает на своим детищем, скорее всего один, а за Visual C++ стоит корпорация с очень немалыми ресурсами. У которой есть желание сделать этот компилятор очень хорошим, ибо на нём в Microsoft написано очень много, если не всё.

И ещё вопрос: можно ли при оптимизации пожертвовать надёжностью ради скорости? Например, есть x+y, но мы не будем проверять переполнение — так быстрее. Или есть x [y], но мы не будем проверять индекс (известный только в рантайме) на выход за пределы границ — так тоже быстрее. В Си не заморачиваются этим и не проверяют. Но у каждого языка своя философия.

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

Читаю сайт Алексея Недори — он делает упор именно на компоненты. Я понимаю, что «время нас рассудит», но при существующих тенденциях вижу грядущее торжество всё более крупных и всё более функциональных компонентов. Всё будет собираться из кубиков, а программисты превратятся в сисадминов, делающих настройки, но не пишущих код. И зеро-кодинг — из той же оперы. Так что Ваша забота об оптимизации мне понятна, но «пипл хавает» и неоптимизированное.

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

✅  2023/04/02 22:44, void          #75 

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

Почитайте, например, докторскую д.ф.-м.н.:

Ковалёв Сергей Протасович. Теорико-категорные модели и методы проектирования больших информационно-управляющих систем. Москва 2013

Там как раз про это. Если коротко: он нашёл способ формализации аспектно-ориентированного программирования через метаматематику, теорию категорий.

Найдены формализмы вроде категории c-DESC,категории "запятой", соответствующие копредел диаграммы соединения (кодекартов коквадрат), морфизмы в категории (коконусы). Морфизмы имеют физический смысл технологии комплексифицирования, составленной из приёмов вроде (загрузка, подстановка, соединение) реализующими технологию сборки, комплексифицирования и т.п. — то есть, методы компонентного программирования. Доказывается ряд теорем в теории категорий с соответствующими объектами. То есть: строятся, конструируются формальные технологии проектирования. Синтезируются технологии конфигурирования (на сонове доказанных теорем). Конструируется формальная технология проектирования вычислительных систем.

Далее формализуется семантика аспектно-ориентированного подхода — через описанные выше методы теории категорий.

АОП, в свою очередь, используется для формализации компонентно-ориентированного, сборочного программирования. Строятся эквивалентные им модели из теории категорий, доказывается ряд наглядных и остроумных теорем. Далее происходит аспектно-ориентированный синтез технологий специфицирования. Синтезируются технологии проектирования, моделирования данных, прецедентов, комплексифицирования. Описываются модели предметной области. Разрабатывается ряд АРМ и АСУП. Приводятся примеры внедрений.

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

Интересная такая вот докторская, про практическое применение теории категорий и матетематизацию системной инженерии более адекватной математикой.

Ещё можете почитать оригинальные работы Фуксмана А. Л. вроде "Теоретические аспекты создания программных систем", где описаны "основы вертикального слоения программ" и "процесс вертикального слоения", "сосредоточенное описание рассредоточенных действий", то есть, "вертикальных слоев"; процесс интеграции; текст интегрированной программы должен быть послойно размеченным. Описываются виды зависимостей слоев, взаимодействие слоев, приведён алгоритм "технологии вертикального слоения". Алгоритм содержит исчерпывающие указания, как проектировать систему, содержащую основу и вертикальные слои. Алгоритм из 15 действий, некоторые выполняются параллельно (асинхронно). Действия для исполнителя человека, довольно высокоуровневые для примитивной реализации бизнес-процессом. Описывается "техника вертикального слоения" и инструментальный комплекс, где описывается технология оформления программы слоя, внесения изменений послойно. Этим оно напоминает @x...@y...@z changeset-ы из классического weave/tangle или cweb Дональда Кнута, в поддержку literate programming, то есть: грамотного программирования.

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

Всё это в целом, в одну и ту же тему: "Лексикона программирования" А. Ершова, словарей форта Лео Броуди, language orientned framework Мартина Фаулера; literate programming как базы понятных алгоритмов, технологии компонентной сборки понятных программ и систем хорошо документированным способом, моделеориентированной разработки; интенсивной, а не экстенсивной технологии программирования; эффективной минималистичной bootstrap самораскрутки системы или дистрибутива или языка программирования в духе метациклического интерпретатора лиспа, пролога или метакомпилятора форта; менее громоздкой, но более наглядной и эффективной технологии программирования и построенной таким образом системы в целом.

✅  2023/04/03 00:01, void          #76 

Ильдар

Но это в Си. А вот у машинных кодов нет выражений! Они выполняются сразу же.

Да элементарно. Вот вам пример:
    ORG    $1000
START: ; first instruction of program

* Put program code here

LEA SRC, A1
LEA DST,A2
MOVE #9, d1 ; a-1
STRNCPY:
MOVE.B (a1)+,(a2)+
DBRA D1,STRNCPY

* PRINT "DST" STRING
lea DST,A1
MOVE #14,D0
TRAP #15

SIMHALT ; halt simulator

* Put variables and constants here

RULER DC.b '0123456789a'
SRC DC.B 'from.THERE',0
DST dc.B 'TO.HERE123',0

END START ; last line of source
Берёте Easy68k отсюда: http://www.easy68k.com/ Ассемблируете пример. И смотрите под отладчиком в память начиная с $1000, строки. Видите, что копируется SRC в DST. И в консоли, выводится естественно,
from.THERE
Там,кстати, довольно высокоуровневый макроязык, с циклами и ветвлениями в духе MASM. Ещё есть DBLOOP D0=9 ... UNLESS, например. (цикл for 9 раз с условием exit, например) Особенно приятно этим пользоваться, учитывая ортогональный набор команд: например, в X86 здесь в
LOOP CX
может быть только выделенный CX, в 68k же в
DBRA Dx,label
— Dx любой регистр. Например, в examples/tutorial3.X68:
*-----------------------------------------------------------
* Title : Tutorial 3 for EASy68k
* Written by : Chuck Kelly
* Date Created : May-06-2004
* Dec-25-2012 by CK, Uses SIMHALT
* Description :
*
* A simple number guessing game. The program generates a random number
* between 1 and 100 and asks the player to guess the number. After each guess
* the program displays higher or lower until the player guesses the correct number.
*
* Demonstrates Structured Assembly, SECTION directive & Macros with EASy68K
*-----------------------------------------------------------
OPT MEX Expand macro calls
OPT SEX Expand structured statements

CODE EQU 0
TEXT EQU 1
CR EQU $0D
LF EQU $0A


SECTION TEXT
ORG $800 set the address for TEXT data

SECTION CODE
ORG $2000 set the address for program code

PRINT MACRO
lea \1,a1
move.b #14,d0
trap #15
ENDM

GETNUM MACRO
move.b #4,d0 get user guess in D1.L
trap #15
ENDM

*----------------------------------------------------
* Program execution begins here
*----------------------------------------------------
START

repeat
PRINT instr_Msg print instructions

* generate random number for player to guess
* Uses timer as random number generator. This may not be the best way to
* generate a random number but it will work for this demonstration.
* Trap code 8 returns time in hundredths of a second since midnight in D1.L

move.b #8,d0
trap #15
and.l #$5FFFFF,d1 prevent overflow in divu
divu #100,d1 time count / 100
swap d1 swap upper and lower words of D1 to put remainder in low word
addq.w #1,d1 d1.w contains number from 1 to 100
move d1,d2 d2 = d1

repeat
PRINT enterMsg Display 'enter guess' prompt
GETNUM get user guess in D1.L

if d1 <ne> #0 then
if d1 <hi> d2 then if the player guessed hi
PRINT bigMsg Display 'Too Big'
else
if d1 <lo> d2 then if the player guessed lo
PRINT smallMsg Display 'Too Small'
else The user guessed the number
PRINT goodMsg Display 'Good Guess'
endi
endi
endi
until d1 <eq> #0 OR d1 <eq> d2 do
until d1 <eq> #0 do

SIMHALT

SECTION TEXT
instr_Msg dc.b CR,LF
dc.b '----- Number guessing game -----',CR,LF
dc.b ' Guess the number from 1 to 100',CR,LF
dc.b ' Enter a 0 to quit the game',CR,LF,CR,LF,0

enterMsg dc.b CR,LF,'Enter your guess: ',0
goodMsg dc.b CR,LF,'Good guess, that is correct!',CR,LF,0
bigMsg dc.b CR,LF,'Your guess is too big',CR,LF,0
smallMsg dc.b CR,LF,'Your guess is too small',CR,LF,0

END START
Вот это довольно высокоуровневые макросы, в духе HLL конструкций MASM-а, например. Ещё в каких-то обучалках про дёмки был пример с двойной косвенной адресацией одной инструкцией в квадратных скобках первый указатель, в круглых — второй, первый ссылается на второй и изменяется связанное.

Сейчас что-то пример сходу не найду, может тоже было макросом сделано. Но например аналог pushal/popal там делался одной инструкцией вроде
    movem.l d0-d7/a0-a6,-(SP) ; PUSH registers
movem.l (SP)+,d0-d7/a0-a6 ; POP registers, a7=SP

✅  2024/04/10 18:12, veector          #77 

void, это не машинные коды, это текстовый язык ассемблера, который транслируется (компилируется) в машинные коды. Машинные коды это просто байты в памяти.

✅  2024/04/13 00:00, alextretyak          #78 

Автор сайта

В PDP машинные операции инкремента и декремента соответствовали исключительно префиксным операциям Си. Никаких постфиксных операций inc/dec в ассемблере PDP быть просто не могло.

Ильдар

Ахинея. Постфиксные инкремент или декремент вычисляются после ...

Ого, сколько тут знатоков ассемблера PDP собралось. Ну ладно, не обижайтесь, но по существу, void оказался прав: в PDP-11 действительно есть постинкремент (постфиксным его называть не очень корректно, т.к. слово «постфиксный» обозначает способ записи). И хотя нужен он, в основном, для загрузки констант в регистры и для реализации выталкивания из стека (для понимания: в x86 инструкция pop eax означает по сути eax = *esp++), но архитектура набора команд PDP-11 позволяет применять постинкремент к любому регистру.

По ссылке на wikibooks.org, которую дал void, полезного мало, да и в целом, его сообщения, очевидно, не располагают к вдумчивому изучению, поэтому приведу простой и наглядный пример ассемблерного кода для PDP-11:
clr r0           ; r0 = 0
mov #000100 r1 ; r1 = 000100
movb (r0)+ (r1)+ ; *r1++ = *r0++
movb (r0)+ (r1)+ ; *r1++ = *r0++
movb (r0)+ (r1)+ ; *r1++ = *r0++
movb (r0)+ (r1)+ ; *r1++ = *r0++
(Обратите внимание, что приёмник указывается после источника, в отличие от Intel-синтаксиса.) Проверить код можно в симуляторе. Я нашёл симулятор PDP-11 на JavaScript, который работает в браузере и его не нужно скачивать/устанавливать. Вот ссылка: https://www.se.rit.edu/~llk/pdp11.html После копирования асм-кода в текстовое поле ‘Code’ нужно нажать кнопку ‘Compile’, а затем ‘Run’. В результате выполнения кода 4 байта по адресам 000100, 000101, 000102 и 000103 (в восьмеричной системе счисления) получат значение первых 4-х байт памяти — это машинный код первых двух инструкций.

Вообще, все инструкции в PDP-11 занимают 2 байта (в том числе и movb (r0)+ (r1)+), но для псевдо-инструкции mov #000100 r1 используется занятный трюк. Т.к. это по сути инструкция mov (pc)+, r1, после которой идёт двухбайтовая константа 000100:
mov (pc)+, r1
.word 000100
Если нажать кнопочку ‘View Binary’, можно заметить, что машинный код для этой пары строк совпадает с тем, что генерируется для одной строки mov #000100 r1.

Для понимания работы этого трюка дам небольшую теоретическую справку (которая, правда, относится к процессорам CISC-архитектуры с инструкциями переменной длины, но в данном случае эта логика подходит и для PDP-11.

pc (program counter, он же r7) — это аналог регистра eip/rip из x86. В нём хранится адрес (номер байта в памяти) текущей выполняемой инструкции. Первая фаза обработки инструкции процессором (если не учитывать fetch) — декодирование. В процессе декодирования инструкции процессор увеличивает значение в регистре pc/eip/rip на величину, равную размеру этой инструкции в байтах (в случае PDP-11 это всегда 2 байта). Таким образом, к началу следующей фазы — выполнения инструкции — регистр pc/eip/rip будет указывать уже на следующую инструкцию. После того, как текущая инструкция будет выполнена, процессор начинает обработку следующей инструкции, т.е. той инструкции, на которую ссылается/указывает новое значение регистра pc/eip/rip. Если в процессе исполнения инструкции значение регистра pc/eip/rip было изменено (например, в x86 инструкция jmp <метка> — по логике это просто mov eip, <адрес_метки>), то следующей исполняемой процессором инструкцией будет та, которой соответствует уже новое значение pc/eip/rip.

Так вот, после декодирования инструкции mov (pc)+, r1, регистр pc увеличивается на 2 и указывает на .word 000100, т.е. на слово памяти в котором вместо команды хранится число 000100. В процессе исполнения инструкции mov (pc)+, r1 это число будет прочитано из памяти и записано в регистр r1. В завершение исполнения инструкции сработает постинкремент и значение регистра pc ещё раз увеличится на 2 (не на 1, т.к. это инструкция mov, а не movb), т.е. процессор как бы «перепрыгнет» слово в памяти, в котором располагается число 000100, и начнёт обработку следующей за ним инструкции.

Этот трюк возможен только благодаря поддержке постинкремента, который в данном случае применяется к регистру pc. В архитектуре ARM64, к примеру, все инструкции имеют размер 4 байта, и чтобы загрузить в регистр 32-х или 64-х разрядную константу приходится выкручиваться с literal pool, либо генерировать цепочку из 16-разрядных mov-ов. Ну а в x86-64 проблема загрузки в регистр 32-х или 64-х разрядных констант решается переменной длиной инструкций.

Примечательно, что постдекремента и преинкремента в PDP-11 нет: есть только постинкремент и предекремент, который обозначается как −(r0).

✅  2024/04/13 12:31, Автор сайта          #79 

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

Поэтому говорить о постдекрементах и преинкрементах совершенно бессмысленно. По форме они префиксные (как на языке ассемблера в синтаксисе Intel, так и на уровне битов операций: коды операций идут впереди операндов), а по семантике они монолитны (что в принципе совпадает с семантикой префиксных «++» и «--» в Си).

А вот постфиксные «++» и «--» в Си вычурны по семантике: их операнды могут принадлежать сразу двум операциям: например, операции присваивания и этим постфиксным операциям, которые выполняются в конце, в последнюю очередь.
 while (*dst++ = *src++);
В этом примере получается, что сперва идёт выборка операндов, потом втискивается присвоение, потом проверка на ненулевое значение, потом выход в случае нулевого значения. И только в самом конце — постфиксные инкременты. Префиксные операции не позволяют такого разбиения на этапы.

✅  2024/04/15 00:00, alextretyak          #80 

Позвольте немного побыть педантом.

Таким образом, к началу следующей фазы — выполнения инструкции — регистр pc/eip/rip будет указывать уже на следующую инструкцию.

Вы немного ошиблись и вставили это предложение не туда. Оно должно располагаться сразу перед предложением «После того, как текущая инструкция будет выполнена...». Ну или сразу после «... (в случае PDP-11 это всегда 2 байта).»

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

Судя по тому, что написано в Википедии (https://en.wikipedia.org/wiki/PDP-11#Models), в первых реализациях архитектуры PDP-11 (PDP–11/20 и PDP–11/15) ещё не применялось микропрограммное управление.

Как реализована инструкция push в процессорах x86 — я точно не знаю (сколько и какие именно микрооперации ей соответствуют), но суть в том, что в PDP-11 нет специальной инструкции push! И логику push необходимо задавать явно в коде инструкции, что позволяет использовать любой другой регистр в качестве вершины стека, а также выбрать нестандартное направление роста стека. И при всём при этом эта логика умещается в одной машинной инструкции (речь, очевидно, идёт об инструкции mov).

К примеру, push сперва заталкивает что-то в стек, а потом изменяет адрес вершины стека.

push eax работает как *--esp = eax, т.е. сначала уменьшает адрес вершины стека, а затем помещает данные по этому адресу. Т.е. образно говоря, в push используется предекремент, а в pop — постинкремент. (И это не только в x86, а такое соглашение принято также в ARM и большинстве других архитектур).

Самый низкий уровень, к которому он имеет доступ, — это машинные операции

Да, именно об этом уровне и идёт речь. PDP-11 поддерживает постинкремент и предекремент на уровне машинных операций. Машинные инструкции PDP-11 с двумя операндами имеют следующий формат:
BOOOsssSSSdddDDD
│└┬┘└┬┘└┬┘└┬┘└┬┘
│ │ │ │ │ └─ 0-2 биты: регистр-приёмник
│ │ │ │ └──── 3-5 биты: режим адресации регистра-приёмника
│ │ │ └─────── 6-8 биты: регистр-источник
│ │ └───────── 9-11 биты: режим адресации регистра-источника
│ └─────────── 12-14 биты: код операции
└───────────────── 15 бит: признак byte-инструкций
Например, инструкция mov r3 r5 кодируется как 010305. 1 — код операции, 03 — источник, 05 — приёмник. Среди всех режимов адресации нам интересны следующие четыре:
┌───┬───┬────────────────┐
│Bin│Oct│Assembler Syntax│
├───┼───┼────────────────┤
│000│ 0 │ Rn │
│001│ 1 │(Rn) │
│010│ 2 │(Rn)+ │
│100│ 4 │-(Rn) │
└───┴───┴────────────────┘
Несмотря на термин «режим адресации», режимы 2 и 4 фактически имеют семантику постфиксного инкремента и префиксного декремента из языка Си. Вот примеры инструкций (в т.ч. с использованием постинкремента и предекремента):
; Во второй колонке соответствующий код на языке Си
; В третьей колонке машинный код в восьмеричной сис. сч.
movb r3 r5 ; r5 = r3 ; 11│0│3│0│5
mov r3 r5 ; r5 = r3 ; 01│0│3│0│5
mov (r3) r5 ; r5 = *r3 ; 01│1│3│0│5
mov r3 (r5) ; *r5 = r3 ; 01│0│3│1│5
mov (r3) (r5) ; *r5 = *r3 ; 01│1│3│1│5

mov (r3)+ (r5) ; *r5 = *r3++ ; 01│2│3│1│5
mov (r3) (r5)+ ; *r5++ = *r3 ; 01│1│3│2│5
mov (r3)+ (r5)+ ; *r5++ = *r3++ ; 01│2│3│2│5

mov (r3) -(r5) ; *--r5 = *r3 ; 01│1│3│4│5
...

add r3 r5 ; r5 += r3 ; 06│0│3│0│5
...
add (r3) (r5) ; *r5 += *r3 ; 06│1│3│1│5
add (r3)+ (r5) ; *r5 += *r3++ ; 06│2│3│1│5
add (r3) (r5)+ ; *r5++ += *r3 ; 06│1│3│2│5
add (r3)+ (r5)+ ; *r5++ += *r3++ ; 06│2│3│2│5
add -(r3) (r5) ; *r5 += *--r3 ; 06│4│3│1│5
...

sub r3 r5 ; r5 -= r3 ; 16│0│3│0│5
...

Поэтому говорить о постдекрементах и преинкрементах совершенно бессмысленно.

Это вопрос терминологии. Называть режимы адресации (Rn)+ и -(Rn) постинкрементом и предекрементом придумал не я. Это хотя и не официальное их наименование (официально в документации DEC они называются автоинкремент и автодекремент), но вполне распространённое:

https://news.ycombinator.com/item?id=24817321:

«pre/post-increment/decrement addressing is baked into the instruction set»

«... 4 is the pre-decrement mode ...»

https://stackoverflow.com/questions/17436141/kr-seems-to-prefer-pre-increment:

«PDP-11 had post-increment and pre-decrement.»

while (*dst++ = *src++);

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

И всё-таки инкременты выполняются перед выходом. А если посмотреть на ассемблерный код, который генерирует компилятор gcc для данного примера (https://godbolt.org/z/zT4EoTh6W), то видно, что инкременты выполняются даже перед присваиванием (причём выполняются полностью — компилятор не только инкрементирует значение в регистрах, соответствующих dst и src, но и сохраняет их в отведённой для них памяти для локальных переменных перед присвоением *dst). Почему компилятору разрешено так поступать — это уже другой вопрос, который разбирать в деталях здесь будет уже неуместно, как я считаю. Вообще, если переписать этот пример с использованием if и goto
begin_while:
bool is_not_zero = (*dst++ = *src++) != 0;
if (!is_not_zero) goto end_while;
// Здесь располагается тело цикла while, но в данном случае оно пустое
goto begin_while;
end_while:
то окажется, что система команд PDP-11 может в точности повторить этот Сишный код:
mov #100 r0 ; r0 - src
mov #200 r1 ; r1 - dst

begin_while:
movb (r0)+ (r1)+ ; bool is_not_zero = (*dst++ = *src++) != 0;
beq end_while ; if (!is_not_zero) goto end_while;
br begin_while ; goto begin_while;
end_while:
halt

; Копируемые данные:
.= 100
.word 123456
.word 007700
.word 000000

; Область памяти назначения забиваем мусором
; (для проверки того, что данные из источника
; скопировались полностью, включая завершающий
; нулевой байт)
.= 200
.word 111111
.word 111111
.word 111111
.word 111111
После выполнения этой программы данные по адресу 100 (включая завершающий нулевой байт) скопируются в память по адресу 200.

✅  2024/04/18 04:47, void          #81 

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

Уместно-уместно. Причина одна, и это тоже один из признаков устаревшего языка: Undefined Behaviour в стандарте, причём UB is not error, а намёки компилятору:

Вот здесь есть дырка в понимании поведения абстрактного исполнителя интерпретатором/модели памяти и верификатора стандарта компилятором.

В нормально, корректно определённом языке на мой взгляд — не должно быть UB в принципе. Как, например, определено в Ada. В С же эта дыра в понимании вполне приемлема — в контексте того, что поведение по умолчанию в случае UB будет такое, как в нижележащем варианте абстрактного исполнителя:
  1. интерпретируемого BCPL, откуда в Си пришла семантика указателей (дырявая),
  2. вот этот пример с ассемблером CISC процессоров PDP-11 либо MC68k.
Налицо протечка абстракций из более нижележащего уровня или просто исторически раннего. Chris Lattner, автор компилятора LLVM где-то писал в рассылке, что на самом деле это мешает. Действительно низкоуровневый язык мог бы более эффективно загрузить вычислительные узлы.

Но у компилятора Си нет такой информации. Из-за UB он должен перебирать варианты, пытаться оптимизировать, самостоятельно догадываться, как именно это UB следует понимать. Что на мой взгляд довольно глупо — более корректно было бы более правильно определить операционную семантику абстрактного исполнителя, затыкая эту дыру в понимании стандарта, чтобы никакого UB в принципе в правильно определённом языке не возникало, по определению.

✅  2024/04/19 22:13, Автор сайта          #82 

alextretyak

И всё-таки инкременты выполняются перед выходом.

Да, здесь Вы правы, когда выход сделан, то поздно пить Боржоми делать инкремент. Но как инкремент может делаться перед присвоением? Возьму-ка я 2 строки. Первая, src:
0019ff20:  20 21 22 23| 24 25 26 00| 28 29 2a 2b| 2c 2d 2e 00 | !"#|$%&.|()*+|,-..|
Вторая, dst:
0019ff30:  21 22 23 24| 25 26 27 00| 29 2a 2b 2c| 2d 2e 2f 00 |!"#$|%&'.|)*+,|-./.|
Выполню этот цикл:
	while (*dst++ = *src++);
Вторая строка с адресом 0019ff30 примет такой вид:
0019ff30:  20 21 22 23| 24 25 26 00| 29 2a 2b 2c| 2d 2e 2f 00 | !"#|$%&.|)*+,|-./.|
Вторая часть строки, от адреса 0019ff30 до адреса 0019ff37 станет такой же, как и первая от адреса 0019ff20 до адреса 0019ff27. А оставшаяся часть строки не изменится. То есть нулевое содержимое байта послужило сигналом на выход из цикла. При этом переменные src и dst содержат после выхода из цикла такие адреса:
src = 19ff28
dst = 19ff38
То есть произошло следующее:
  • были скопированы байты 0 — 7, включая седьмой с содержимым 0,
  • содержимое 0 седьмого байта послужило сигналом в выходу; этот факт процессор запомнил,
  • был выполнен постинкремент переменных src и dst и они стали отличиться от исходных значений не на 7, а на 8!
  • и, наконец, выход.
То есть постинкременты делаются после присваивания! Да и как они сделались бы после инкремента? Ведь тогда бы произошло присвоение 8-го байта! А этого не было.

Так же важно отметить, что в Си, в отличие от машинных кодов, постинкремент делается по окончанию выполнения выражения. Это свойство позволяет впихивать в выражение дополнительные действия. Например, можно выполнять не присваивание, а «+=», «*=», «&=» и т.п.
	while (*dst++ += *src++);
Вот как выглядит строка после «+=»:
0019ff30:  41 43 45 47| 49 4b 4d 00| 29 2a 2b 2c| 2d 2e 2f 00 |ACEG|IKM.|)*+,|-./.|
Но машинные команды монолитны, в их так называемые постинкременты невозможно втиснуть дополнительные действия, какие сделаны выше на Си. Чтобы ещё раз подтвердить это, изменим пример вот так:
int  F (int  i) { return  i & 1; }
. . .
while (*dst++ += F(*src) * *src++);
И получаем результат:
0019ff30:  21 43 23 47| 25 4b 27 00| 29 2a 2b 2c| 2d 2e 2f 00 |!C#G|%K'.|)*+,|-./.|
Так что постфиксный инкремент в Си позволяет втискивать перед финальным увеличением много чего. В отличие от машинной команды, которой нельзя передать другую машинную команду, которую надо выполнить до завершения первой. Поэтому правильно Вы говорите, что для машинных кодов постинкременты — это вопрос терминологии. Видимо разработчики, реализуя в железе машинные команды, вынесли терминологию со своей кухни на всеобщее обозрение.

void

это тоже один из признаков устаревшего языка: Undefined Behaviour в стандарте, причём UB is not error

Так и есть!!! Спасибо, что Вы взяли меня за руку и подвели к слону, которого я и не заметил. И сам нарывался на неопределённое поведение и возмущался этим. И коллеги. И сколько пишут про это. Это должно идти первым пунктом в этой статье! А я не замечал слона в центре комнаты. И Ada, конечно, хороший пример.

❔  2024/04/23 00:00, alextretyak          #83 

void

Уместно-уместно. Причина одна, и это тоже один из признаков устаревшего языка: Undefined Behaviour

Undefined behaviour — штука, конечно, скверная. Вот только в коде while (*dst++ = *src++); нет никакого undefined behaviour. У выражения *dst++ = *src++ вполне себе определённое поведение и его производимый эффект не зависит от порядка выполнения инкрементов и присваивания, т.к. инкрементируются и присваиваются различные сущности: инкрементируется dst, а присваивается *dst. Вот если написать dst++ = src++, тогда здесь будет UB, т.к. dst меняется более одного раза (в ++ и в =) между соседними точками следования. (*dst)++ = (*src)++ — это также UB. И ++dst = 0. И dst++ = 0. Но не *dst++ = 0!
Подробнее можно почитать здесь: https://stackoverflow.com/a/4176333/2692494 (там же есть комментарий, почему *p++ = 4 не является UB)

Автор сайта

То есть произошло следующее:

• были скопированы байты 0 — 7, включая седьмой с содержимым 0,
• содержимое 0 седьмого байта послужило сигналом в выходу; этот факт процессор запомнил,
• был выполнен постинкремент переменных src и dst и они стали отличиться от исходных значений не на 7, а на 8!
• и, наконец, выход.

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

Давайте я разберу ассемблерный код, который сгенерировал компилятор gcc (ссылку я уже приводил: https://godbolt.org/z/zT4EoTh6W).
.L2:
mov rdx, QWORD PTR [rbp-8] ; rdx = src ; prev_src = src
lea rax, [rdx+1] ; rax = rdx+1
mov QWORD PTR [rbp-8], rax ; src = rax ; ++src
mov rax, QWORD PTR [rbp-16] ; rax = dst ; prev_dst = dst
lea rcx, [rax+1] ; rcx = rax+1
mov QWORD PTR [rbp-16], rcx ; dst = rcx ; ++dst
movzx edx, BYTE PTR [rdx] ; dl = *rdx
mov BYTE PTR [rax], dl ; *rax = dl ; *prev_dst = *prev_src
movzx eax, BYTE PTR [rax] ; al = *rax
test al, al ; if (al != 0)
jne .L2 ; goto .L2
(Оптимизации были выключены, чтобы компилятор генерировал максимально понятный код.)
Как можете сами убедиться, оба инкремента (src++ и dst++) выполняются перед присвоением. Как же тогда запись осуществляется по старому значению dst, если dst инкрементируется перед присваиванием? Всё просто: компилятор хранит копию неинкрементированного значения src и dst в регистрах.

Вот цитата из стандарта C++ (раздел 7.6.1 Postfix expressions, 7.6.1.5 Increment and decrement):

The value computation of the ++ expression is sequenced before the modification of the operand object.

Google-перевод:

Вычисление значения выражения ++ выполняется до модификации объекта операнда.

Помните я приводил код функции postfix_increment() в [этом сообщении](http://compiler.su/postfiksnye-inkrement-i-dekrement.php#25)?
Компилятор делает буквально следующее:
while (*postfix_increment(dst) = *postfix_increment(src));
Т.е. сначала полностью выполняется правая сторона присваивания — функция postfix_increment(src), при этом значение src инкрементируется, а предыдущее (неинкрементированное) значение, которое вернула функция (назову его prev_src), сохраняется в регистре. Затем полностью выполняется левая сторона присваивания — postfix_increment(dst). И только потом присваивание *prev_dst = *prev_src, и значение *prev_dst сравнивается с нулём.

Добавить свой отзыв

Написать автору можно на электронную почту
mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Содержание

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

●  Изобретение очередного велосипеда?

●  Все языки эквивалентны. Но некоторые из них эквивалентнее других

●  Признаки устаревшего языка

●  Лень — двигатель прогресса

●  О наименовании проекта и языка программирования

Анализ и критика

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Новости и прочее




Последние отзывы

2024/11/21 11:02 ••• Автор сайта
Продолжение цикла и выход из него

2024/11/12 20:24 ••• Вежливый Лис
Правила языка: строки, комментарии

2024/11/12 13:10 ••• Вежливый Лис
Новости и прочее

2024/11/12 00:32 ••• Автор сайта
Оценка надёжности функции с несколькими реализациями

2024/11/06 02:50 ••• Иван
Энтузиасты-разработчики компиляторов и их проекты

2024/11/05 23:51 ••• Борис К.
Изменение приоритетов операций

2024/11/05 23:38 ••• Борис К.
Шестнадцатиричные и двоичные константы

2024/11/04 12:50 ••• Неслучайный читатель
Русский язык и программирование

2024/11/01 12:11 ••• ИванАс
Русской операционной системой должна стать ReactOS

2024/10/27 14:01 ••• Автор сайта
О русском ассемблере

2024/09/29 23:40 ••• Автор сайта
Десятка худших фич C#

2024/09/29 13:10 ••• Автор сайта
ЕС ЭВМ — это измена, трусость и обман?

2024/09/22 21:08 ••• Вежливый Лис
Бесплатный софт в мышеловке