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

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

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

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

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

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

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

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

            Рискую повторить классификацию Борхеса, но попробую описать формальные критерии отнесения того или иного языка к устаревшим. И так, каковы они, эти признаки или приметы? Это когда в языке есть:
  • оператор «goto»;
  • такая обработка исключений, которая ещё хуже «goto», когда исключение неизвестно где возникает и неизвестно куда передаёт управление;
  • постфиксные операции «++» и «--», которые для большинства — загадочны;
  • висячий «else»;
  • приведения типов, влекущие «тайное» изменения значения;
  • нулевой указатель и обращения по абсолютным адресам;
  • упор на мутабельные состояния;
  • возможность присвоить неинициализированное значение;
  • визуальный мусор типа «begin» и «end», особенно ЗАГЛАВНЫМИ буквами;
  • синтаксис, прогибающий под себя программистов, а-ля Forth;
  • развитый макроязык, который говорит о неразвитости собственно языка.
И когда в языке нет:
  • ключевых слов а-ля PL/1;
  • приоритетов операций а-ля Lisp или Forth;
  • контроля границ массивов а-ля Си;
  • контроля возможного переполнения при арифметических операциях;
  • функций — объектов первого класса;
  • оператора «for each»;
  • программирования в стиле доказательств;
  • вывода типов;
  • зрительных ориентиров в тексте, позволяющих отличить операции от операндов;
  • синтаксически обособленных чистых функций.
            Если программирование будет идти вперёд, то в том числе через новые языки с новыми подходами и парадигмами. Для того, чтобы оставаться в рамках старых парадигм и привычных подходов, новые языки не нужны. Для этого есть «старый добрый» Си или «тёплый ламповый» Паскаль. Новые Си и Паскаль с иной, пусть и эргономичной, с отшлифованной формой записи проиграют конкуренцию своим первоисточникам.

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

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

Опубликовано: 2019.09.05, последняя правка: 2019.09.12    18:48

ОценитеОценки посетителей
   ██████████████████████████████████████████ 2 (100%)
   ▌ 0
   ▌ 0
   ▌ 0

Отзывы

     2019/09/10 10:17, kt          # 

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

     2019/09/10 16:42, MihalNik          # 

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

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

     2019/09/10 16:48, kt          # 

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

     2019/09/10 18:36, MihalNik          # 

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

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

     2019/09/10 21:10, kt          # 

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

     2019/09/10 22:25, MihalNik          # 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Но поскольку язык активно используется, расширения должны сохранять обратную совместимость, поэтому максимум что доступно — рекомендовать новые практики взамен старых. Например, в современном 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          # 

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

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

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

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

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

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

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

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

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

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

     2019/09/16 13:06, kt          # 

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

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

     2019/09/16 18:13, MihalNik          # 

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

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

     2019/09/16 18:38, MihalNik          # 

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

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

     2019/09/16 21:03, kt          # 

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

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

Предпочитаю явно указать, что а — это вещественное с мантиссой 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          # 

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

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

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

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

     2019/09/17 11:11, kt          # 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

     2019/09/18 15:18, kt          # 

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

Это при линейной структуре исходного текста. А если стоят ветвления? Например:
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, Автор сайта          # 

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

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

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

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

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

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

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

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

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

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

Подчёркивание моё. Я имел ввиду то, с чем борятся 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          # 

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

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

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

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

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

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

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

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

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

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

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

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

     2019/10/26 10:00, utkin          # 

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

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

     2019/10/26 10:10, utkin          # 

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

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

     2019/10/26 14:47, kt          # 

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

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

     2019/10/28 08:17, utkin          # 

Я имел ввиду, что лучше явно указать разрядность, чем писать все эти совершенно непонятные "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          # 

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

Я писал не о необходимости самому рассчитывать число разрядов, а о форме явного указания длины объектов в памяти. В 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          # 

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

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

     2019/10/28 17:49, utkin          # 

Вдруг где-то будут не 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, Автор сайта          # 

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

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

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

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

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

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

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

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

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

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

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

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

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

     2019/10/29 07:42, MihalNik          # 

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

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

     2019/10/29 10:02, utkin          # 

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

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

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

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

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

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

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

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

     2019/10/29 10:17, utkin          # 

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

     2019/10/29 10:32, kt          # 

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

Это очень старые данные. Даже старые Дельфи (еще до Эмбекадеро) уже во всю равнялись на 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, Автор сайта          # 

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          # 

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

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

А вот зачем данные выравнивать на 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          # 

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

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

Это архитектурный вопрос. Прочитать 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          # 

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

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

     2019/10/30 12:41, MihalNik          # 

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

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

     2019/10/30 14:15, utkin          # 

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

При создании больших программ используются менеджеры памяти. Они распределяют память под данные программы.
https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D0%BD%D0%B5%D0%B4%D0%B6%D0%B5%D1%80_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8
Память в современных программах давно распределяют они. И распределяют её не байтами и битами, а блоками в одну-несколько страниц. А страница уже кратна 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          # 

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

     2019/10/30 18:30, utkin          # 

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

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

     2019/10/30 18:38, utkin          # 

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

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

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

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

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

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

     2019/10/31 08:58, kt          # 

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          # 

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

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

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

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

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

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

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

     2019/10/31 12:47, utkin          # 

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

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

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

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

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

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

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

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

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

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

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

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

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

Никаких «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 Маздайщик          # 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

     2019/10/31 14:28, kt          # 

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

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

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

Знаете, когда-то я читал такое: «Хватит выравнивать на границу слова или двойного слова, это было важно для быстродействия во времена первых Пентиумов, но не сейчас. В современных 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          # 

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

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

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

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

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

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

     2019/11/01 19:38, kt          # 

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

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

Автор сайта:

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

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

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

Правильнее было бы сделать несколько замеров (минимум десяток), усреднить результаты и посчитать доверительный интервал. Тогда процент имеет смысл считать. Сейчас цифра 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          # 

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

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

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

Авторизация

Регистрация

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

Карта сайта


Содержание

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

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

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

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

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

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

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

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

Компилятор

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

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

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

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




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

2019/12/08 17:48 ••• Noname
Почему обречён язык Форт

2019/12/05 23:29 ••• Автор сайта
Слоны в комнате

2019/12/03 22:45 ••• Автор сайта
Следующие 7000 языков программирования: заключение

2019/12/03 22:36 ••• Автор сайта
Наблюдаемая эволюция языка программирования

2019/11/30 20:55 ••• Сергей
Каким должен быть язык программирования?

2019/11/09 21:27 ••• kt
Программирование без программистов — это медицина без врачей

2019/11/07 10:58 ••• kt
Признаки устаревшего языка

2019/10/28 23:55 ••• Автор сайта
Типы в инженерных задачах

2019/10/15 16:32 ••• kt
Модификация исполняемого кода как способ реализации массивов с изменяемыми границами

2019/10/07 14:15 ••• Автор сайта
О наименовании проекта и языка программирования

2019/09/19 15:23 ••• kt
Некошерный «goto»

2019/09/13 16:38 ••• Автор сайта
Программирование исчезнет. Будет дрессировка нейронных сетей