Почему Кнут использует этот неуклюжий декремент?

Я смотрю на часть кода профессора Дона Кнута, написанного на CWEB, который преобразован в C. Конкретным примером является dlx1.w, доступный по адресу веб-сайт Кнута

На одном этапе значение .len структуры nd[cc] уменьшается, и это делается неуклюжим способом:

  o,t=nd[cc].len-1;
  o,nd[cc].len=t;

(Это вопрос, относящийся к Кнуту, поэтому, возможно, вы уже знаете, что «o» — это макрос препроцессора для увеличения «mems», что представляет собой промежуточную сумму затраченных усилий, измеряемую доступом к 64-битным словам.) значение, оставшееся в «t», определенно не используется ни для чего другого. (Пример здесь находится в строке 665 файла dlx1.w или в строке 193 файла dlx1.c после ctangle.)

Мой вопрос: почему Кнут пишет именно так, а не

nd[cc].len--;

который он на самом деле использует в другом месте (строка 551 dlx1.w):

oo,nd[k].len--,nd[k].aux=i-1;

(И «oo» — это аналогичный макрос для двойного увеличения «mems», но здесь есть некоторая тонкость, потому что .len и .aux хранятся в одном и том же 64-битном слове. Чтобы присвоить значения S.len и S. aux, обычно засчитывается только одно приращение к mems.)

Моя единственная теория состоит в том, что декремент состоит из двух обращений к памяти: сначала для поиска, а затем для назначения. (Правильно ли это?) И этот способ написания является напоминанием о двух шагах. Это было бы необычно многословно для Кнута, но, возможно, это инстинктивная памятная записка, а не дидактика.

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


person Ed Wynn    schedule 30.12.2018    source источник
comment
Все, что делает Кнут, неуклюже и оптимизировано для путаницы.   -  person Boann    schedule 31.12.2018
comment
@Boann Мне любопытно, что заставляет тебя так говорить; не могли бы вы уточнить? Лично я всегда находил работы Кнута ясными и восхитительными, начиная с моего первого знакомства (Конкретная математика) и заканчивая несколькими его статьями и разделами Искусство компьютерного программирования. > (Его стиль программирования, похоже, отличался от мейнстрима 1970-х годов, т. е. он нашел решения, отличные от большинства других, но интересно, о чем именно вы говорите.)   -  person ShreevatsaR    schedule 01.01.2019
comment
@ShreevatsaR Я удивлен, что кто-то так говорит! Я изо всех сил пытался добиться каких-либо успехов с TAOCP, но сдался и выбросил книгу. Для меня Кнут - это тот, кто действительно хочет, чтобы в программировании было меньше языка и больше абстрактной развлекательной математики, поэтому он притворяется, что это так, несмотря на то, что это (для меня) совершенно запутанно и непрактично. Посмотрите на эти абсурдные имена переменных в вопросе; o, t, nd, cc, aux. Это непостижимо.   -  person Boann    schedule 01.01.2019
comment
@Boann Ну, ваш опыт принадлежит вам, и я не могу его оспаривать, но ИМО Кнут - самый «реальный» из математиков / алгоритмистов: тот, кто наиболее явно не притворяется, что абстракция реальна, но анализ того, что может произойти с реальными программами на реальных компьютерах (например, он анализирует не только асимптотику Big-O, но и вплоть до постоянного множителя). Конечно, TAOCP посвящен математическому анализу алгоритмов (см. предисловие), но IMO более «конкретен», чем любой из его преемников/альтернатив (например, какие из них включают ассемблерные программы для изучения влияния кэша, размера ОЗУ, конвейерной обработки и т. д.?)   -  person ShreevatsaR    schedule 03.01.2019


Ответы (2)


Предварительное замечание: при грамотном программировании в стиле Кнута (т. е. при чтении программ WEB или CWEB) «настоящая» программа в понимании Кнута — это не «исходный» .w файл и не сгенерированный (запутанный) .c файл, а набранный (тканый) выход. Исходный файл .w лучше всего рассматривать как средство для его создания (и, конечно, также исходный код .c, который передается компилятору). (Если у вас нет под рукой cweave и TeX, я набрал некоторые из этих программ здесь; эта программа DLX1 находится здесь.)

Так что в этом случае я бы описал место в коде как модуль 25 DLX1 или подпрограмму «крышка»:

вопрос

В любом случае, возвращаясь к основному вопросу: обратите внимание, что это (DLX1) — одна из программ, написанных для The Art of Computer Programming. Поскольку отчет о времени, затраченном программой на «секунды» или «минуты», из года в год становится бессмысленным, он сообщает, сколько времени заняла программа, в количестве «мемов» плюс «упс», в котором преобладают «мемы», т. е. количество обращений к памяти к 64-битным словам (обычно). Так вот в книге есть утверждения вроде «эта программа находит ответ на эту задачу за 3,5 гигабайта времени выполнения». Кроме того, утверждения предназначены в основном для самой программы/алгоритма, а не для конкретного кода, сгенерированного конкретной версией компилятора для определенного оборудования. (В идеале, когда детали очень важны, он пишет программу в MMIX или MMIXAL и анализирует ее работу на аппаратном обеспечении MMIX, но это случается редко.) Целью вставки инструкций o и oo является подсчет мемов (для отчета, как указано выше). в программу. Обратите внимание, что более важно сделать это правильно для инструкций «внутреннего цикла», которые выполняются много раз, например, все в подпрограмме cover в этом случае.

Это подробно описано в Разделе 1.3.1' (часть Faccicle 1):

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

Для практических целей время выполнения MMIX программы часто можно удовлетворительно оценить, назначив фиксированную стоимость каждой операции на основе приблизительного времени выполнения, которое было бы получено на высокопроизводительной машине с большим объемом оперативной памяти; так что мы будем делать. Предполагается, что каждая операция принимает целое число υ, где υ (произносится как «упс») — это единица измерения, представляющая время тактового цикла в конвейерной реализации. Хотя значение υ уменьшается по мере совершенствования технологии, мы всегда идем в ногу с последними достижениями, потому что мы измеряем время в единицах υ, а не в наносекундах. Время работы в наших оценках также будет зависеть от количества обращений к памяти или мемов, которые использует программа; это количество инструкций загрузки и сохранения. Например, мы предположим, что каждая инструкция LDO (загрузить окта) стоит µ + υ, где µ — средняя стоимость обращения к памяти. Общее время работы программы может быть выражено, скажем, как 35µ+ 1000υ, что означает «35 мемов плюс 1000 oops». Отношение µ/υ неуклонно растет в течение многих лет; никто точно не знает, сохранится ли эта тенденция, но опыт показал, что µ и υ заслуживают независимого рассмотрения.

И он, конечно, понимает отличие от реальности:

Несмотря на то, что мы будем часто использовать допущения из Таблицы 1 для ориентировочных оценок времени выполнения, мы должны помнить, что фактическое время выполнения может быть весьма чувствительным к порядку выполнения инструкций. Например, целочисленное деление может стоить всего один цикл, если мы сможем найти 60 других действий между моментом, когда мы выдаем команду, и временем, когда нам нужен результат. Некоторым инструкциям LDB (загрузка байта) может потребоваться обращение к памяти только один раз, если они ссылаются на один и тот же октабайт. Однако результат команды загрузки обычно не готов к использованию в следующей инструкции. Опыт показал, что одни алгоритмы хорошо работают с кэш-памятью, а другие нет; поэтому µ на ​​самом деле не является постоянной величиной. Даже расположение инструкций в памяти может существенно повлиять на производительность, потому что одни инструкции могут быть получены вместе с другими. […] Только мета-симулятору можно доверять, чтобы он давал достоверную информацию о фактическом поведении программы на практике; но такие результаты может быть трудно интерпретировать, потому что возможно бесконечно много конфигураций. Вот почему мы часто прибегаем к гораздо более простым оценкам таблицы 1.

Наконец, мы можем использовать Compiler Explorer от Godbolt, чтобы просмотреть код, сгенерированный типичным компилятором для этого кода. (В идеале мы бы посмотрели на инструкции MMIX, но, поскольку мы не можем этого сделать, давайте остановимся на значении по умолчанию, которое выглядит как x68-64 gcc 8.2.) Я удалил все os и oos.

Для версии кода с:

  /*o*/ t = nd[cc].len - 1;
  /*o*/ nd[cc].len = t;

сгенерированный код для первой строки:

  movsx rax, r13d
  sal rax, 4
  add rax, OFFSET FLAT:nd+8
  mov eax, DWORD PTR [rax]
  lea r14d, [rax-1]

а для второй строки:

  movsx rax, r13d
  sal rax, 4
  add rax, OFFSET FLAT:nd+8
  mov DWORD PTR [rax], r14d

Для версии кода с:

  /*o ?*/ nd[cc].len --;

сгенерированный код:

  movsx rax, r13d
  sal rax, 4
  add rax, OFFSET FLAT:nd+8
  mov eax, DWORD PTR [rax]
  lea edx, [rax-1]
  movsx rax, r13d
  sal rax, 4
  add rax, OFFSET FLAT:nd+8
  mov DWORD PTR [rax], edx

что, как вы можете видеть (даже не зная много о сборке x86-64), является просто конкатенацией кода, сгенерированного в первом случае (за исключением использования регистра edx вместо r14d), так что это не так, как если бы запись декремента в одной строке сохранил вам все мемы. В частности, было бы не корректно считать его за один, особенно в чем-то вроде cover, который в этом алгоритме вызывается огромное количество раз (пляшущие звенья для точного покрытия).

Таким образом, версия, написанная Кнутом, верна, поскольку ее целью является подсчет количества мемов. Он также может написать oo,nd[cc].len--; (считая два мема), как вы заметили, но, возможно, в этом случае на первый взгляд это может показаться ошибкой. (Кстати, в вашем примере в вопросе oo,nd[k].len--,nd[k].aux=i-1; два мема происходят из загрузки и хранилища в --, а не из двух хранилищ.)

person ShreevatsaR    schedule 30.12.2018
comment
Хороший ответ! -- спасибо за приложенные усилия. Я до сих пор не знаю, почему Кнут не написал oo,nd[cc].len--;. (Он мог предположить, что читатели поймут, что декремент стоит двух мемов.) Я согласен, что это небольшой стилистический вопрос. (Я не был уверен, что он такой маленький, когда впервые спросил.) - person Ed Wynn; 30.12.2018
comment
@EdWynn Да… Еще один секрет: иногда грамотное программирование — это просто метод организации кода, и это не обязательно означает, что к программе было применено много «отшлифовки», т.е. грамотные программы все еще могут быть написаны наспех, иметь ошибки, и т. д. Здесь возможны разные вещи, например. когда он писал это, он думал о чем-то другом, возможно, ожидал добавления дополнительных инструкций между ними, возможно, хотел, чтобы компилятор не оптимизировал хранилище, которое происходит немедленно… вероятно, это не имеет значения :-) - person ShreevatsaR; 31.12.2018

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

person R.. GitHub STOP HELPING ICE    schedule 30.12.2018
comment
Одна из причин моего вопроса заключалась в том, чтобы проверить, что неуклюжий метод не дает другого эффекта, возможно, для какого-то странного пограничного случая, который я не могу себе представить. (И S.len, и t имеют тип int, поэтому места для крайних случаев ограничены.) Вы сосредоточились на подсчете памяти, что подразумевает, что эффект представляет собой простое уменьшение. Хорошо, я могу расслабиться насчет эффектов. - person Ed Wynn; 30.12.2018
comment
Memcount не обязательно должен быть основан на правильной модели, чтобы быть полезным. Это может быть прагматически прокси-мерой для требуемой работы таким образом, который не зависит от архитектуры/компилятора/системы. Таким образом, это очень полезно, и я не знаю лучшей меры. Недавно я сравнил количество мемов с процессорным временем в некоторых тестовых случаях: зависимость была линейной. (Время ЦП сейчас значит даже меньше, чем когда-либо, поэтому я эффективно измерял тактовое время без каких-либо других пользовательских процессов.) С ветвлением, предварительной выборкой и кэшированием любые реальные измерения сильно зависят от системы, поэтому их трудно воспроизвести. - person Ed Wynn; 30.12.2018