[МУЗЫКА] [МУЗЫКА] Я хочу вам кратко пояснить, что такое статические языки программирования. Статические языки программирования — такие, в которых трансляторы знают виды обрабатываемых значений в каждой точке программы. Вот те самые Алгол 68, Ада, Модула-2 Паскаль в его лучших поздних проявлениях. Был такой язык еще CHILL — это базовый язык Международного Консультативного Комитета по Телеграфии и Телефонии. Вот во всех этих языках транслятор знает виды обрабатываемых значений в каждой точке программы. Скажем, в Фортране, в PL/I или в C это далеко не так. Во многих языках можно описать процедуру с тремя параметрами, а вызвать с двумя или с четырьмя, и никто слова не скажет. Это же ужасно, то есть человек ошибся, а подсказки у него нет. В данном случае ужасно, что сам компьютер не может проверить данные. А в статических, если программа написана на статическом языке, то компьютер, как и транслятор, может проверить все, что угодно. Так вот, мы решили остановится на классе одних языков — только статических. И договорились, что все проверки, максимально возможное количество проверок мы делаем во время трансляции, а во время счета ничего не повторяем. И вот поэтому «Самсон» получится маленьким, компактным, без всякого водяного охлаждения. Так вот, я хочу сказать, что тут польза идет в две стороны: как я уже сказал, транслятор помогает аппаратуре, например, проверки берет на себя, так и аппаратура может помочь транслятору. Например, мы строго следим за ортогональностью системы команд. Если у нас есть четыре типа адресации и 13 типов действий, то будет строго 52 команды. Причем были случаи, когда какие-то молодые сотрудники ко мне приходили и говорили: «Андрей, вот такая-то команда практически никогда не используется, очень редко используется. Давай ее выкинем». Я говорю: «Отлично, давай, только вот этот кусок транслятора, где используется эта команда, будешь писать лично ты». Сразу вопросы пропадали. Ортогональность — великое дело. Мы слишком сильно настрадались на той же самой ЕС ЭВМ — копии IBM 360. Например, для полуслов там есть сложение, вычитание, умножение (ah, sh и mh), а команды dh, деления, нет. И поэтому когда ты пишешь транслятор и программируешь формулы, если встретится деление, тебе надо делать какую-то подпрограмму. Это кошмар. Или другой пример: для полных слов, 32-разрядных, сложение и вычитание требует по одному регистру, а умножение и деление — по два. Вы представляете, что такое распределять регистры в трансляторе, когда надо иногда хватать один регистр, а иногда — два, причем первый должен быть четным. Это тоже сильно затрудняет работу транслятора. Поэтому мы на это пойти никак не могли и наставили на полную ортогональную систему команд. «Самсон» — это HLL-машина. И поэтому он, как все HLL-машины, имеет стек, даже не один, а три. Мы разделили стеки: стек целых, стек вещественных, стек целых — 16 позиций по 16 разрядов, не забудьте, что это было в 80-е годы. Стек вещественных — 8 позиций по 32 разряда, и стек адресов — 16 позиций по 24 разряда. В то время максимальная память у нас была 16 МБ, то есть 24 бита. В те годы это было недостижимо. Я не помню ни одного «Самсона», в котором было реально 16 МБ памяти. Вот максимально сегодня уже молодые сотрудники требует не 2, а 4 или 8 ГБ памяти. В общем, мне нынешнюю молодежь, как любому старику, трудно понять. В те годы 16 МБ для нас было недостижимой мечтой. Так вот, транслятор аккуратно знает, когда каким стеком пользоваться — он по типам операнда видит, что здесь надо работать со стеком целых, здесь со стеком вещественных, здесь со стеком адресных. Более того, во всех HLL-машинах есть такое понятие как исчезновение стека, когда позиция стека — стек вот так — и если указатель стека ушел вниз, за стек. Или переполнение стека, когда указатель стека ушел за стек. Так вот, в Самсоне таких действий нет. Я сказал, и тут же немножко осекся. Каждый код находится какой-то один, редко два умных студента говорят: «Профессор, вы говорите какую-то ересь. Как это нет переполнения стека? А если будет рекурсия, вы же не можете посчитать глубину рекурсии во время трансляции?». Это правда. Я всегда таких студентов хвалю и говорю: «Да, есть единственное исключение — команда вызов процедуры. Вот она действительно проверяет, что позиции стека хватит. Если не хватает, она отгружает какие-то регистры в память». Но это ровно в одной команде, а не во всех командах, как это происходит во всех других HLL-машинах. И поэтому, значит, стековая архитектура полностью обеспечена компилятором: когда, каким стеком пользоваться, где указатель стека находится. Ведь мы во время трансляции любой процедуры можем полностью посчитать ее дыхание, как мы говорим, стека, то есть глубину стека, на сколько позиций стека она займет в максимальном виде. Стек может вдохнуть, выдохнуть, как мы говорим. Так вот, глубину дыхания мы можем для каждой процедуры посчитать во время трансляции, но еще раз повторяю: конечно, мы не можем это сделать между вызовами процедуры. Теперь перейдем к обсуждению работы с памятью УВК «Самсон». Мы в самом начале хотели, чтобы в УВК «Самсон» была виртуальная память. Я вам рассказывал уже в предыдущих лекциях, что это такое — выдуманная память, когда человек думает, что у него ГБ, а реально только МБ. И тем не менее это сильно помогает в программировании, и есть способы как не слишком сильно затормозить машину при этом. Так вот, при работе с виртуальной памятью возникает всегда дилемма: использовать страничную память или сегментную. Страничная память — это когда память выделяется страницами одинаковой длины, скажем, 4000 слов или 4000 байтов. А сегментная организация — это когда память выделяется сегментами переменной длины. Так вот, если вы завели сегментов 4000 байтов, а использовали в ней только 100 байтов, то остальные байты пропадут. Это называется внутренняя фрагментация. И бороться с внутренней фрагментацией никто до сих пор не научился. А в сегментной организации возникает другая проблема. Вы занимали-занимали память, а потом — раз, в серединке какой-то сегмент освободили. Возникает свободный кусок памяти — вы не знаете, какой длины. И в результате такое действие называется внешняя фрагментация. Так вот, с внешней фрагментацией уже научились бороться — есть так называемый метод граничных признаков, когда каждый сегмент ограничивается двумя словами, по одному слову с каждой стороны. И причем там [НЕРАЗБОРЧИВО] длина сегмента, и если плюс, то это занятый сегмент, а если минус длина сегмента, то это свободный сегмент. И тогда можно соседние сегменты клеить, просто если раздвинуть два сегмента, то ты этот хочешь освободить. Ты можешь всегда посмотреть, соседний свободен или нет — их можно склеить. Таким образом, с внешней фрагментацией можно успешно бороться. Поэтому в «Самсоне» мы приняли решение организовать память в виде сегментной памяти, с сегментами переменной длины. Каждый процесс имеет как минимум два сегмента: сегмент кода и сегмент данных. И мы считаем, что главным в этих является сегмент данных — дело в том, что мы, конечно, используем то, что раньше называлось реитерабельной только на чтение. В сегмент кода никто никогда не пишет. Ада Августа, когда вводила первые программы, она вводила понятие переадресации, чтобы можно было прямо в сегмент кода что-то менять. Допустим, введешь в [НЕРАЗБОРЧИВО], добавляешь 1 к адресу. Но оказалось, что это чрезвычайно плохо: вот эта идея Ады Августы в жизни не подтверждена. Сегодня все стараются делать код, чистый по записи, то есть в него никогда никто не пишет. Тогда один сегмент кода, он используется сотнями параллельно протекающих процессов — просто разные экземпляры процессов. И вот, знаете, вот у нас процесс представляется сегментом данных, а в нем есть ссылка на сегмент кода, который исполняет это. И еще раз повторю, что вполне возможно, что сотни сегментов данных ссылаются на один и тот сегмент кода — ничего страшного, в него все равно никто ничего не пишет. В сегменте данных размещается стек статик функция, то есть каждая функция в начале работы хватает себе кусочек памяти. Если сегмент данных переполняется, ничего страшного — операционная система выделит больший сегмент, а все старые данные перепишет. Поэтому за этим мы не следим. На начало глобальных данных указывает регистр G (Global) — он один, а вот сегмент локальных данных меняется от вызова к вызову. Так вот, на статику текущей процедуры, которая сейчас исполняется указывает регистр L (Local). УВК «Самсон» использует безадресную систему команд. Что это значит? Как в риск-машинах: то есть мы всю работу с памятью сосредоточили в двух командах: Load и Store, то есть из памяти загрузить в стек нужный. Load для целых, Load для вещественных, Load для адресных — разные, для каждого стека своя команда загрузки и, соответственно, своя команда выгрузки. Так вот, команды Load и Store загружают нужный стек, а все команды остальные — арифметические, логические, — все остальные команды устроены одинаковым образом: они берут нужное количество операндов, чаще всего два, из нужного стека, выполняют операцию, кладут результат в стек. Давайте рассмотрим несколько примеров. Если там плюс: вот он берет два операнда из стека, целых допустим, и кладет результат туда. Команда сравнения вещественных: a < b, где a и b — вещественные. Берется два операнда из стека вещественных, а результат 0 или 1, истина или ложь, пишется в стек целых. Есть унарная операция, например −1. Берется один операнд из целых или из вещественных, откуда надо. И туда же кладется результат отрицания: −операнд. Есть унарная операция NOT, или по-русски «не» — отрицание логическое. Берется со стека целый один операнд, вычисляется операция NOT и туда же кладется результат. Так вот, я легко теперь могу буквально за несколько секунд рассказать все команды целочисленные или [НЕРАЗБОРЧИВО] арифметики. То есть обычно: +, −, ×, ÷. У нас, кстати, вот когда в мнемокоде, когда мы пишем сами на мнемокоде, вообще-то, на самом деле, пользователи «Самсона» не пишут никогда — когда писались трансляторы, операционные схемы какие-то фрагменты [НЕРАЗБОРЧИВО] мы, авторы «Самсона» писали, а пользователю это не разрешаем. Так вот, мы так и писали: у нас знак операции так и обозначается — знаком +, −, ×, ÷. Соотвественно, для плавающих, для вещественных чисел — это +p, −p, ×p и ÷p. Вот здесь я хочу рассказать вам про одну оптимизацию, мелкую, но очень важную. Смотрите, пусть вам встретилась формула: a − f(x). Заметьте: если было бы a + f(x), то никаких проблем не было бы. Вот пусть вам встретилась именно a − f(x). Вы загрузили a на стек, потом долго-долго считаете f(x). a лежит себе на стеке и занимает важный ресурс — в стеке всего 16 позиций, а одну вы заняли. Конечно лучше было бы как-то сделать сначала f(x), а потом в a. Но если мы будем делать f(x), потом загружать a, потом делать отдельную команду минус, будут лишние байты и лишний так. Мы вот придумали такой, на мой взгляд, хитрый прием, а именно ввели обратную операцию для некоммутативных операций: минус обратный, и плюс обратный, и делить обратный. То есть в формуле a − f(x) сначала вычисляется f(x), потом загружается a, а потом команда минус обратный — получается нужный результат. Если посмотреть на листинг любой программы самсоновской, обратные операции встречаются даже чаще, чем прямые. Пустяк, а приятно. Вот из таких пустяков и создается оптимальный код программы.