Нуссс. Начну из далека. На заре вычислительной техники народ довольствовался механическими вычислительными машинками. Не буду на них останавливаться - это тема для длинющего поста, начиная с камешков на берегу и заканчивая машинкой "Энигма".
Потом, почти одновременно, появились аналоговые вычислительные машины - АВМ. Они имели крайне узкую направленность, и грубо говоря пытались электронными, а зачастую и физическими методами повторить некий физ. процесс и получить результат/расчёт того или иного физ. процесса. Головоломка уже не для программистов, а для физиков и математеков.
штЫрьками собираем схему для нужного вычислеия - соединения подключают различные блоки интеграторов, делителей, усилителей пр. Всё должно быть выполнено очень точно. Это пик развития аналоговой электроники
Про квантовые компы я промолчу тихи в тряпочку - сколько ни читал - муть мутная и мне непонятная. Стар я видать стал, а может стал и СуперСтар...
Говорить я буду о привычных компах - которые работают с единичками и ноликами (троичные погибли, хотя в СССР были нехилые наработки). Вроде всё норм, но комп, процессор, понимает как раз енти нули и единички. Ещё на этапе проектирования железяки народ понял енту проблему и начал объединять их в группы, кратные степени двойки. Так появился байт (2^3 = 8), а потом и слово (2^4 = 16). И тут же различные производители железа придумали первый геморрой для программистов - порядок следования байт в слове. Little-Endian и Big-Endial - эти слова знакомы каждому сетевику и привычно вызывают когнитивный диссонанс...
Но ладно, вернёмся к ассемблеру. На заре программирования под цифровые процессора народ вбивал программу тумблерами - битик за битиком. Производительность труда была на наименьшейшем уровне, а вероятность щёлкнуть не тот битик самой большой.
Нервы не выдержали и мисс Кэтлин Бут решила облегчить себе работу, создав первый в мире ассемблер для ARC2 (военный авиационный вычислитель).
Эту идею подхватили остальные. Что самое интересное - на языке Ассемблера можно добиваться максимальной производительности (самая популярная библиотека ffmpeg имеет множество ассемблерного кода под разные архитектуры) - выжать из процессора все соки. Такое же происходит в различных математических библиотеках.
Вроде всё с ассемблером хорошо, кроме одного - там команды процессору заменены на мнемокоды - краткие символьные представления команды процессора. Писать программу стало удобно, понятно. Появились кросс-ассемблеры (низкий уровень llvm)
Но оснавная засада осталась - трудоёмкость программирования и половина башки программиста занята архитектурой компа (да-да, там не только про проц надо помнить, но и про периферию).
спёрто с https://blog.skillfactory.ru/glossary/assembler/
А что итого? Я понимаю, что питонисты нифига не поймут - они даже не понимают, что каждая питон-либа это просто обёртка под высокоэффективным кодом на Си. Ассемблер жив и будет жив, те же "тысяча строк" ассемблера под линуксом - это и есть основа ядра ОС. Потом уже можно писать на Си, Си++, даже Расте (чёт ОС растоманов зачахла - видать пришло понимание, что язык высокого уровня не может использоваться наравне с Ассемблером или СИ).
Язык Ассемблера, как продился, так и будет жив. Есть яыки программирования близкие к АСМу - тот же Си, или Форт, но никто не даст программисту той мощи и контроля за компом как АСМ. Тот же Си в своих диалектах везде имеет возможность АСМ-вставок.
Что же до программирования на АСМе - мне он нравится, но... Всегда надо иметь баланс в голове. Поверьте strncpy() из Си будет более оптимизирована чем ваша реализаций на АСМе, а вот к-либо формула, вычисляющая интеграл, которого нет в clib - лучше уже на асме, особенно если оно вычисляется в цикле..
Я всегда работал в командах, где рядом были гики. Они могли часами ковыряться в исходниках, спорить о фреймворках, разбирать архитектуры до последнего байта. И я часто чувствовал себя самозванцем.
Я не получал удовольствия от бесконечного копания в технологиях. Меня всегда больше заводила другая часть - продукт. Чтобы было что показать людям. Чтобы оно работало и решало задачу.
И вот недавно я наткнулся на простую мысль: есть два типа разработчиков.
Гики— им в кайф технологии ради технологий. Они кайфуют от деталей, оптимизаций, исходников.
Продуктовые— им интересен результат. Технологии для них — инструмент, а драйвит то, что продуктом кто-то реально пользуется.
Когда я это понял — многое встало на свои места. Я перестал сравнивать себя с теми, кто живёт кодом ради кода. Я нашёл отдушину в пет-проектах и indie-hacking. Там как раз важнее другое — скорость, гипотезы, первые пользователи.
💡 Поэтому если ты тоже чувствуешь синдром самозванца — попробуй честно ответить себе: кто ты? Гик или продуктовый. И тогда сравнивать станет проще, а работать легче.
Я делюсь такими наблюдениями про код, проекты и indie-hacking у себя в телеге. Если эта тема отзывается — заглядывайте, ссылка в профиле
О проекте: Пишем один код - собираем на разные 8 бит МК!
https://vm5277.ru - это универсальное решение для embedded-разработки, которое позволяет сократить время создания прошивки для 8 бит микроконтроллеров в разы.
Как это работает?
Пишешь код на Java подобном языке (чистое ООП, без головной боли с указателями и не читабельным кодом)
Компилятор автоматически генерирует оптимизированный ассемблерный код под выбранную платформу
Код работает поверх легковесной RTOS, написанной на ассемблере для максимальной производительности
Ассемблер-сборщик финализирует проект в бинарный файл прошивки
Что входит в решение:
Высокоуровневый компилятор с Java подобным синтаксисом
Максимально оптимизированная RTOS, целиком на ассемблере для каждой платформы
Унифицированные драйвера - один API для UART, SPI, I2C, GPIO на всех МК
Стандартные библиотеки (Runtime): Базовые типы данных (Float, String), математические функции (Math), работа с памятью и другие абстракции, не зависящие от платформы.
Драйверы верхнего уровня (High-Level Drivers): Унифицированные API для работы с периферийными устройствами: датчиками (I2C/SPI), дисплеями, SD-картами, беспроводными модулями (ESP8266, Bluetooth).
Ключевые преимущества:
Скорость разработки: Создавайте функционал на высокоуровневом языке быстрее, чем на Си
Производительность: RTOS и драйверы оптимизированы до уровня чистого ассемблера, экономя каждый байт и такт
Переносимость: Переход с AVR на PIC или STM8 - дело нескольких правок, а не изучение и адаптация библиотек
Проект находится на ранней стадии, но я активно над ним работаю. Уже можно видеть, как высокоуровневый код на Java-подобном языке превращается в чистый и эффективный ассемблер! Это ещё не итоговый вариант, но прогресс уже есть.
Что уже работает в этом примере:
Наследование интерфейсов: Test implements Number
Динамическое выделение памяти: оператор new
Полиморфизм: вызов метода через переменную интерфейсного типа (Number.toByte())
Проверка типа во время выполнения: оператор is (аналог instanceof)
Полноценная работа с объектами: конструкторы, методы, поля.
Интеграция с RTOS: вызов системных функций (System.out).
Исходный код custom.j8b
Итоговый ассемблер код(стр. 1, черновой, могут быть ошибки)
Итоговый ассемблер код(стр. 2, черновой, могут быть ошибки)
Также приведу одну из функций RTOS(код сырой, может содержать ошибки)
1. Метаданные класса: Компилятор автоматически формирует структуру для поддержки RTTI (Run-Time Type Information), необходимую для instanceof.
2. Динамическое создание объекта в куче: Код конструктора new Byte(0x08) транслируется в вызов менеджера динамической памяти (os_dram_alloc) и инициализацию полей.
3. Проверка типа (is / instanceof):
Оператор if(b1 is Byte) компилируется в вызов процедуры j8bproc_instanceof_nr, которая проверяет метаданные объекта.
4. Полиморфный вызов метода:
Вызов b1.toByte() через интерфейс Number преобразуется в универсальный механизм поиска и диспетчеризации метода.
5. Интеграция с системными сервисами:
Вывод в "консоль" (System.out) — это вызов системного сервиса ОСРВ.
Что это значит?
Это доказывает, что подход vm5277 работоспособен. Мы можем писать на высокоуровневом ООП-языке, а под капотом получать код, который:
Эффективно использует память: объекты размещаются в куче, метаданные компактны.
Сохраняет производительность: ключевые операции (выделение памяти, проверка типов) вынесены в оптимизированные ассемблерные процедуры.
Является переносимым: этот же Java-код, после завершения работы над платформами, сможет работать на PIC и STM8 (ограничений почти нет).
Высокочувствительный модуль цвета TCS3472, который способен определять RGB цвета (красный, зеленый, синий), и светить именно тем светом, цвет предмета которого был поднесён к датчику, потребляя при этом доли миллиампер и имеет широкую область применения. Кстати, стоит такой 138 руб. Ссылка на него
Многие новички в программировании совершают эту ошибку — пытаются написать всё и сразу. Говорю им, что нужно разбивать на подзадачи и писать маленькими порциями — всё равно пишут всё сразу, а потом ищут ошибки сразу по всему коду. Вместо того, чтобы отлавливать их на маленьких кусочках.
Начинающим вообще каждые 2-3 строчки лучше запускать. 10 строк кода уже много, уже тяжеловато в них ориентироваться. Но вот пишут, делают ошибки, искать их и исправлять ещё не умеют, поэтому при исправлении часто только ещё сильнее ломают... Сколько мучений на ровном месте.
Одна из частых ошибок молодых программистов — экономия времени на чтении. Читают текст недостаточно внимательно, не понимают, что требуется, как именно должен быть оформлен результат — и создают вообще не ту программу, тратя кучу времени.
Читать нужно раз 5. Даже если там 2 строчки — 5 раз хотя бы. Тогда шансов так ошибиться гораздо меньше.
Привет. Я продолжаю разрабатывать сервер для Lineage 2 C1 на JavaScript Проект
При добавлении SoulShot функционала не добавил проверку не только на наличие оружия, но и кто атакует — игрок или NPC.
Как итог теперь все атакуют с помощью SoulShot.
Я решил создать игру при помощи нейросети. Читал, что люди
заморачиваются, используют по несколько нейросетей для разных задач,
максимально подробно всё описывают. Мне было впадлу. Поэтому я скинул
всё на chatgpt. "Разработай 3д песочницу с возможностью строительства.
Вся разработка на тебе" Что я могу сказать. Chatgpt вообще не
приспособлен для такого. Он пишет каждый раз код с нуля. Когда
количество строк подходит к 300, он начинает удалять часть кода и
ставить заглушки.
Следующим я скачал windsurf. Это редактор основанный на vs
code с встроенной нейронкой swe-1. (там есть ещё, но они за кредиты. А
мне дали за регистрацию бесплатный доступ к swe-1 на две недели) Уже
получше. Выбираешь папку и она даже сама может создавать файлы. Даже
ctrl-c ctrl-v делать не надо. каааайф. Swe-1 уже не удаляет предыдущий
код. Он может работать с файлами в несколько тысяч строк. Минусы:
пиздец какой медленный. Куча багов. За 5 часов мы создали человечка в
космосе, корабль. Подошли к управлению кораблем и потом игра вконец
сломалась. На экране полнейший пиздец, он потратил 100+ сообщений на
исправление багов и у него нихуя не вышло. Думаю, если проявить
терпение, то впринципе в теории он может сделать игру, но... за это
время проще самому выучить программирование и самому всё сделать
Далее я купил подписку на copilot. У него такие же проблемы.
Он работает побыстрее, но после определенного момента игра ломается.
Багов столько, что он их правит по 100-200 сообщений и у него нихуя не
получается.
Ещё у copilot есть лимиты. И также хочу сказать, что я
сталкивался с лимитом даже на модель 4.1. хотя на самом сайте майкрософт
написано, что лимитов нет.
Sorry, you have exhausted the agent mode usage limit. Please switch to ask mode and try again later.
Майкрософт пидарасы.
А ещё если вы хоить ненадолго зайдете с русского ip в свой
аккаунт, его заблочат. Я случайно открыл vs code без впн и мне ак
заблокировали. Пришлось второй аккаунт покупать. Microsoft вдвойне
пидарасы.
Могли, как теже openai просто не давать доступ с рос.
айпи и всё. Я бывает случайно chatgpt открываю без впн, они просто не
дают доступ и всё. Включаю впн и свободно пользуюсь. Нахуй сразу аккаунт
блочить? Мрази ебаные.
Я устал от нейросетей... Пойду расслаблюсь, посмотрю боку но пико. Прикоснусь к гениальному. Мне сказали, что там сюжет неплохой. Попробую обзор на него сделать
Сразу хочу внести ясность. Я не экстремист и не пропагандирую ЛГБТ. Я за Россию и Боженьку Путина. Я ему даже регулярно молюсь. Не сажайте плз
Всем, кто захочет покичиться своими знаниями лингвистики, рекомендуется ознакомится со словарями и комментариями к посту
Наверное невозможно найти человека, который бы никогда не встречал программ на Java. Сначала телефоны с J2ME, потом Android, который использует свои варианты реализации JVM и свой байткод. Даже многие сим-карты внутри используют свою оптимизированную разновидность Java.
Даже JavaScript обязан своим названием популярности Java, хотя и не имеет с ней прямого родства. (При этом некоторый промежуток времени существовали Java-апплеты)
3 миллиарда устройств уже 18 лет исполняют программы на Java
По официальной информации, 23 мая 1995 года, ровно 30 лет назад, Sun Microsystems выпустили первую бета-версию Java. Первая полноценная официальная версия JDK 1.0 выйдет только 23 января 1996.
Изначально язык должен был называться "Oak" (дуб, который рос рядом с офисом разработчиков), после чего был переименован "Green" (зеленый), а потом в честь кофе с острова Ява. А в качестве целей языка ставились:
Простота, объектно-ориентированность и знакомость.
Надежность и безопасность.
Независимость от архитектуры и переносимость.
Высокая производительность.
Интерпретируемость, многопоточность и поддержка динамического программирования.
Синтаксис был практически полностью позаимствован из C++, который уже был знаком многим программистам. Были выброшены процедурные артефакты C в виде "бродячих" глобальных переменных и функций, всё должно принадлежать классам. Также выбросили некоторые "неудачные" решения C++, к примеру дружественные классы, множественное наследование (его заменили интерфейсы), перегрузку операторов (зачем?!) и еще немного. Вместо ручного управления памятью было решено внедрить сборщик мусора, чтобы облегчить разработку и снизить количество ошибок и уязвимостей.
Главной особенностью языка стала концепция "напиши единожды, запускай везде" (хотя правильнее это будет назвать "напиши единожды, отлаживай везде" из-за различных реализаций и окружения), возможная благодаря тому, что код сначала компилируется в стандартизированный и независимый от процессора байткод для виртуальной машины (JVM), а в машинный код переводится только на машине потребителя, учитывая её архитектуру и особенности. Получается что-то среднее между интерпретируемым и компилируемым языком.
Вместо Swift там должен быть Rust
Язык получился... интересным.
То, что планировалось простым, в итоге превратилось в многословное нечто, а в сочетании со всякими архитектурными извращениями стало притчей во языцех о громоздкости корпоративного стиля. Потом появились совместимые с JVM альтернативы по типу Kotlin, ибо Java старательно игнорирует и не вводит ничего, что бы могло замазать многословность и неудобство. Android давно официально рекомендует использовать его вместо Java.
Добиться высокой скорости на интерпретируемом языке со сборкой мусора тоже проблемно. JIT замедляет запуск и жрет память, AOT поддерживается плохо и с ним невозможно реализовать некоторые вещи (кодогенерацию, к примеру), плюс остается сборщик мусора. Плюс некоторые не очень удачные решения и ограничения, тоже не способствующие высокой производительности (видно на контрасте с C#, в котором их исправили).
Тем не менее, Java на очень долгое время стала самым востребованным языком и отлично продолжает жить и сейчас, находясь на 4 месте популярности по версии TIOBE и дважды становясь языком года (в 2005 и 2015).
«Языки делятся на те, которые все ругают, и на которых никто не пишет» - Бьерн Страуструп
Хотел написать про работу системы управления памятью, но понял, что для полноценной статьи у меня не хватает практических знаний. Поэтому кратко
Куча
Баян
Помимо стека, о котором было рассказано в прошлой статье и который содержит локальные переменные, в "потребительской" программе в среднестатистической ОС еще бывают глобальные переменные и динамическая память. Глобальные переменные описываются внутри сегментов .bss и .data, резервируясь в памяти сразу после загрузки образа программы (по-правильному это называется процессом).
Динамическая память иначе называется кучей, так как представляет из себя кучу динамических данных, который могут появляться и исчезать во время работы.
Для работы с динамической памятью в C существует malloc(size_t), запрашивающий (выделяющий, аллоцирующий) у ОС кусок памяти указанного размера и возвращающий указатель на первый байт, и free(void *), принимающий этот указатель и возвращающий (освобождающий) кусок назад в ОС.
В C++ есть операторы new и delete, они устроены гораздо сложнее и я не буду их рассматривать, ибо не пишу на нем. Помимо запроса памяти у ОС они занимаются ее инициализацией.
Если по какой-то причине стандартной библиотеки языка в наличии не имеется, то в Windows за память отвечает HeapAlloc и устаревшие GlobalAlloc и LocalAlloc из времен Windows 3.11 (malloc к ним и обращается внутри. Еще есть парные HeapFree, GlobalFree и LocalFree). Они предоставляют больше контроля над результатом, но проприетарны для Windows. А полный список функций для работы с памятью в WinAPI есть здесь, их сотни на любой случай жизни. Можно даже внутри контекста чужого процесса выделить или освободить кусок памяти (обычно такое используется вирусами и прочей нечистью).
Утечки
А что будет, если постоянно выделять, но не освобождать? А еще лучше: выделять, но терять адреса выделенной памяти. ОС достаточно быстро подскажет правильный ответ.
В теории есть несколько возможных выходов, можно даже обработать подобный случай и, к примеру, экстренно освободить всё выделенное (только если не терять указатели) или попытаться спасти нужные данные из разваливающейся программы, но если никаких действий не предпринять, то ОС заставит процесс экстренно прекратить свою деятельность и существование.
Случай с потерей указателей на выделенную память называется утечкой памяти и ее последствия необратимы. Но как только ОС завершает выполнение процесса, то вся его память, даже утекшая, становится снова свободной.
Ленивые программисты и мусоросборка
Отслеживание правильности выделения и освобождения памяти является достаточно сложной и опасной задачей, особенно если требуется обеспечивать стабильность и безопасность (лучше даже не думать, что будет, если рукожопый программист забьет на MISRA и устроит утечку памяти в подсистеме тормозов в Тесле), и практически невыполнимой в некоторых парадигмах (особенно в функциональном программировании). Поэтому еще с древнейших времен (с 1959 года) существуют языки со сборкой мусора:
Особенности
Первое: в таких языках указателей или нет вообще, или они очень ограничены в пользовании (C#).
Второе: для работы с динамической памятью вместо указателей применяются ссылки. Ссылка по факту представляет из себя тот же указатель, только очень сильно ограниченный. Над ссылками запрещены все операции, кроме присваивания значения другой ссылки и разыменования, в то время как указатели имеют права и возможности обычных целых чисел.
Третье: Каждый выделенный кусок памяти всегда занят каким-то объектом.
Четвертое: такие языки всегда тянут за собой подсистему сборки мусора, которая самостоятельно учитывает каждый объект и выделенный под него кусок памяти, а еще подсчитывает количество активных ссылок на каждый подчиненный объект. Это называется подсчетом ссылок.
Пятое: как только процент использованной памяти переходит через порог, то запускается сборщик мусора, который сканирует все объекты и количество существующих на них ссылок. Если ссылок на объект нет (то есть он "утек"), то его память освобождается.
Минусы и проблемы
Дорого. При злоупотреблении сборщик мусора может запускаться слишком часто, а каждый его запуск вносит заметный лаг. Особенно критично это в играх.
Он не устраняет все возможные способы вызвать утечку памяти, к примеру просто выделяя бессмысленные данные, но сохраняя на них ссылки. Или если объект, который управляется сборщиком мусора будет содержать в себе то, что сборщиком мусора не управляется (открытые файлы или нативная память вне его подчинения).
А еще он изредка может удалять то, что удалять не нужно, поэтому появляются костыли уровня GC.KeepAlive() (а-ля самый маленький метод в C#).
Но несмотря не это, практически все современные языки используют сборку мусора, кроме C++ и Rust. C++ по большей части перешел на умные указатели, которые тоже считают количество активных ссылок, но делают это без сборщика мусора. А Rust использует свой уникальный Borrow Checker, который понимают только растофилы.
Не рекомендуется принимать данную статью близко к сердцу или как истину в последней инстанции. Скорее как научпоп-грелку для мозгов. Тут довольно много упрощений, упущений или просто странно написанных моментов. Если что-то объяснено совсем криво, то добро пожаловать в комментарии.
Все сказанное далее применимо везде, но детали описаны для защищенного режима x86 (только я опустил префиксEдля регистров) и языков семейства C (в основном C и C++, местами C#). Для понимания рекомендуется знать про указатели и базово представлять, что происходит в процессоре.
Приятного чтения.
Думаю все помнят, что такое функция в математике
Начнем со сложного
Глоссарий
Чувствую, что большинство не имеет ни малейшего представления об указателях
В части про x86 я упоминал о том, что процессор представляет из себя бешенный самоуправляемый калькулятор, которому указом может быть разве что хранилище кода, аппаратный сброс и немаскируемое прерывание. И что у него есть системная шина, состоящая из шины адреса, шины данных и шины управления, к которой подключена память, хранящая данные и код.
А память это огромная одномерная полоска, в которой каждая ячейка имеет свой порядковый номер от 0 до 2^(разрядность шины адреса)-1. И указатель представляет из себя самую обычную численную переменную, хранящую номер (адрес) любой ячейки.
Я не понимаю, почему для многих это настолько сложная тема, а существенное количество новичков забрасывают изучение C после встречи с ними. Они в своей сути максимально элементарны (до тех пор, пока не нужно исправлять уязвимости и ошибки, которые появились в результате злоупотребления или неполного понимания нюансов).
Также существуют ссылки, так или иначе представляющие из себя подвид указателей, но обычно с запретом на арифметику с ними (основная сложность и опасность указателей) и/или подсчетом количества активных ссылок для сборки мусора.
Все языки или имеют указатели (Pascal, C, C++, блоки unsafe в C# и Rust), или являются ссылочными (Java, C#, Python, JS, т.д. (почти все современные языки)). С++ тоже имеет ссылки, но как синтаксический сахар над указателями, призванный упростить их передачу в функции и избавиться от проблемы нулевых указателей.
Ассемблер - если вы даже примерно не знаете, что это такое, то первую часть статьи вероятно можно пролистать и перейти на обсуждение парадигм.
Регистр - именованная численная переменная внутри процессора. Может иметь особое предназначение, а может просто использоваться для хранения любых чисел и арифметики. Их немного.
Инструкция - одиночная команда, представляет из себя последовательность из нескольких байт со специальным значением. Любая программа представляет из себя последовательность инструкций внутри памяти. Указатель на инструкцию, которая будет выполнена следующей, содержится в регистре IP, который сам увеличивается после выполнения каждой инструкции. По-хорошему стоило нарисовать пошаговую наглядную анимацию с демонстрацией всех регистров и куском ассемблерного кода, но мне лень, а в гугле я ничего толкового не нашел.
Метка - константа, содержащая адрес чего-либо. К примеру процедуры или глобальной переменной. Примерно как метка для goto в высокоуровневых языках, но универсальнее. Применяются при написании на ассемблере, в процессоре как таковые не существуют и разрушаются до обычных чисел при ассемблировании и линковке.
Разыменование - операция над указателем, когда тот превращается в переменную с адресом, который был записан в указателе (своеобразный пульт Д/У для переменной).
Парадигма - "стиль" написания и постройки архитектуры программы, частично определяется языком. Технически на том же C можно писать в практически любой парадигме (через костыли можно писать в стиле ООП, через макросы реализовать метапрограммирование, а через нестандартные расширения вообще ядерный бред), но родная для него - процедурная. А Go, к примеру, хоть и имеет недоклассы и методы, но лишен практически всех благ ООП и не сильно далеко ушел от C.
Синтаксический сахар - необязательная возможность, которая сокращает количество кода, повышает его читаемость или удобство поддержки
Стек
Большинство процессоров Фон-Неймановской архитектуры в своей конструкции предлагают механизмы стека. Кто играл в покер должны вспомнить стеки фишек. То есть некие значения, сложенные друг на друга (обычно это переменные, в частности адреса в памяти). При этом основными операциями являются добавление фишки на вершину (инструкция PUSH) и ее снятие (инструкция POP). Еще можно косвенно читать и заменять (перезаписывать) фишки относительно вершины (на вершину указывает регистр SP, неявно обновляется через POP и PUSH) или основания (указывает регистр BP) вглубь.
Стеки можно переключать (но не в MOS6502), это нужно для многозадачности
Помимо хранения локальных переменных стек позволяет делать довольно интересную вещь: мы можем положить на стек все необходимые аргументы (x в математике, но их может быть несколько), адрес следующей инструкции (взяв из указателя инструкции IP, это будет адрес возврата), после чего совершить прыжок на какой-нибудь другой адрес (сохранение адреса и прыжок делаются инструкцией CALL).
А на этом адресе может быть функция. Сначала она кладет текущий BP на стек, после чего приравнивает основание к вершине (BP к SP), тем самым создав для себя "новый стек" сразу после предыдущего (это еще называется стековым кадром), в котором якобы лежит только значение старого основания стека (BP), чтобы можно было восстановить его перед возвратом.
Серое это стековый кадр от предыдущей функции
После чего функция может прочитать переданные ей аргументы относительно основания своего стека вниз (технически это будет выход за границы текущего стека), выполнить с этим какие-либо действия (записать в файл, вывести в консоль, просто перемножить) и сохранить результат (обычно результат сохраняется не на стек, а в регистр AX).
После чего функция восстанавливает старое значение BP, сняв его со стека, и выполняет команду RET, которая снимает со стека адрес возврата и совершает переход на него, тем самым переключившись на инструкцию сразу после CALL.
И эта система так или иначе перекочевала в большинство высокоуровневых языков начиная с FORTRAN. К примеру в C CALL превратился в круглые скобки, RET в return, а адреса и метки в имена (при этом без скобок они все еще являются указателями, то есть адресами).
Пример кода (к сожалению, штатного форматирования не предусмотрено):
#include <stdio.h> int pow2(int x) { return x * x; } int main() { int y = pow2(16); // Вызов функции. В y будет сохранено число 256 printf("%p", pow2); // Без скобок вместо вызова просто выведет адрес функции return 0; // Возврат нуля из главной функции означает отсутствие ошибок. }
Такой стиль программирования называется процедурным (выделение кода в блоки называется структурным). А вот называть процедурный язык функциональным совершенно неправильно, ибо функциональное программирование ≠ процедурное, они даже в разных категориях (императивное и декларативное).
А теперь скомпилируем этот код и разберем ассемблерный листинг
Важно понимать, что стек в большинстве архитектур традиционно растет от больших адресов к меньшим (и в x86). То есть для того, чтобы отодвинуть его вершину вверх, от регистра SP нужно отнимать значения, а вот для ужимания и съедания ненужных значений к указателю на вершину значения прибавляют. И для доступа к значениям относительно основания или вершины это тоже важно учитывать. Это может звучать запутанно, но через время привыкаешь и всё становится очевидным.
Надеюсь это возможно будет разобрать. Код скомпилирован MSVC v19.28 для x86 со стандартными настройками в Godbolt
Post scriptum
Это всё довольно упрощенно. Как минимум, в защищенном режиме используется больше 5 разных соглашений о вызове, которые отличаются деталями реализации. Это было описание для cdecl, обычно используемого в C. Еще часто используются соглашения pascal, fastcall, thiscall, winapi и другие. Fastcall, к примеру, избегает хранения аргументов на стеке, если их возможно передать через регистры, что улучшает производительность. А winapi отличается от cdecl тем, что функция сама очищает стек от аргументов для себя при возврате. А еще я упустил, к примеру, сохранение регистров, которые функция может перезаписать и испортить, а потому обязана предварительно сохранить и перед возвратом восстановить, передачу переменного количества аргументов (как в printf) и возврат значений шире 32 бит (которые не влезут в EAX).
Плюс сейчас мало кто компилирует ПО под защищенный 32-битный режим, а в длинном режиме (AMD64) используется пара других соглашений, основанных на fastcall и имеющих несколько отличий друг от друга.
Так процедура или функция? Или подпрограмма?
Процедурное программирование предлагает делить код на подпрограммы, которые принято называть функциями и процедурами (функция обычно является наиболее понятным, частым и обобщенным названием, поэтому я его использую).
Процедура от функции отличается только тем, что функция возвращает какое-то значение (как в математике), а вот процедура этого не делает. Не во всех языках явно есть процедуры (в Pascal есть, но не в C). В таком случае их заменяют функции, возвращающие ничего (void, Unit, undefined, None).
Хотя и тут есть свои особенности. К примеру функция, возвращающая void в C и Java является прямым аналогом процедур, как-либо использовать возвращенное значение из такой функции невозможно, ибо его нет физически. А вот Unit в Kotlin это синглтон (а-ля единственная и уникальная константа уникального типа), ссылку на который можно присвоить в переменную, но в этом особого смысла нет. Undefined в JS и None в Python тоже уникальные константы специальных типов.
Но не тут-то было
Вроде бы процедура никогда не может ничего вернуть...
Только она этого никогда не делает напрямую. При она этом может записать результат в глобальную переменную, а еще часто принимает в себя указатели или ссылки, по которым может записать результат. Это еще удобно тем, что можно "вернуть" несколько значений. Пример:
void procedure(int x1, int *x2, int *x3) { // Функция ничего не возвращает, то есть это процедура *x2 = x1 * x1; // Разыменовываем указатель и записываем по его адресу результат. *x3 = x1 * x1 * x1; // Разыменовываем другой указатель и записываем по его адресу результат. } int y1, y2; procedure(16, &y1, &y2); // В y1 оказался результат, аналогичный прошлому примеру. А в y2 куб числа.
PS: оператор звездочка при указании типа превращает его в тип-указатель, а при применении на переменную-указатель разыменовывает ее до изначальной переменной. Амперсанд превращает переменную в указатель на нее (иногда еще называется оператором получения адреса).
То есть мы вернули сразу 2 разных значения из процедуры, которая якобы ничего не возвращает. Чудеса. Подобные чудеса есть в том числе в Pascal с явным делением на процедуры и функции (плюс там это сделано немного удобнее). Хотя механизм тут отличается от того, который используется в возврате значения из функции и совпадает с механизмом передачи обычных аргументов, поэтому никакой магии.
Еще про связь с математикой
Главное отличие функций в программировании от функций в математике в том, что они могут делать что-то на стороне и не обязаны возвращать одинаковый результат при одинаковых аргументах.
К примеру функция получения случайного числа по определению не может существовать в математике, если она не принимает в себя предыдущее случайное число или зерно для его видоизменения. Или функция записи в файл, возвращающая 0 в случае успеха и другое число при провале. Ко всему прочему, такая функция имеет побочный нематематический эффект, то есть запись в файл, что тоже недопустимо традиционной математикой без высоких абстракций.
Поэтому придумали чистые функции. По сути это ограничитель, которые делают функцию полным отражением таковой в математике. Им запрещено возвращать разные значения при одинаковых аргументах (точнее запрещено всё, что может такое позволить сделать), запрещено обращаться к нечистым функциям, запрещено обращаться к тому, что не является аргументом или локальной переменной, запрещены вообще любые действия, которые могут сделать что-то на стороне (даже функция sin() в C не всегда является чистой, ибо может зависеть от состояния FPU).
Чистые функции через ключевое слово pure явно есть в D и FORTRAN (проверка на чистоту во время компиляции), а также являются основой функционального программирования.
Чистая процедура тоже имеет право на жизнь, используя механизм со ссылками (на счет указателей не уверен из-за возможности арифметики над ними).
Функциональное программирование
Это очень сложная категория, которую постоянно путают с процедурным программированием. А еще это де-факто противоположный стиль: декларативный. Традиционное императивное программирование детально описывает процесс получения результата, а декларативное сам результат, без деталей реализации (хотя разделение обычно довольно нечеткое). При этом второй типичен для языков разметки типа HTML и CSS. То есть, условно, как одна и та же операция могла бы выглядеть в императивном и декларативном стиле:
document.tags.A.color = "blue" /* Императивный (JSSS). Сделать ссылки синими */
a { color: blue } /* Декларативный (CSS). Ссылки должны быть синими */
Почувствуйте разницу.
И функциональное программирование я никогда не изучал и слишком мало о нем знаю. Так что готовьтесь к ошибкам и не воспринимайте всё за чистую монету.
Внутри чистого функционального программирования
Основано полностью на математике, все функции обязаны быть чистыми. Операция присваивания запрещена (разрешены константы), переменных в привычном виде нет. Прикольно? Очень!
Во многих процедурных языках функции и процедуры являются объектами второго класса (не путать с классами из ООП), что не позволяет их свободно присваивать в переменные, передавать как аргументы в другие функции или возвращать из них (только через указатели). Функциональные языки расценивают функцию как объект первого класса, то есть их можно, а часто нужно передавать в другие функции напрямую.
Это дает некоторые преимущества, особенно в плане безопасности и при работе с многопоточностью (по причине неизменяемости данных и отсутствия глобального состояния), но вся концепция имеет один фатальный недостаток: вы мало чего полезного можете сделать, ибо что ввод, что вывод являются математически нечистыми, а потому запрещены. Вот такое вот гениальное изобретение безумных математиков.
Функциональное и процедурное программирование. Холст, масло
Каждый чисто функциональный язык выкручивается из этого по-своему, к примеру через монады. Это позволяет им существовать вне шуток и даже использоваться на практике.
Это не все особенности функциональных языков, но одни из самых важных. Самый известный такой язык: Haskell. Функциональные F#, Lisp, ML и многие другие не являются 100% чистыми.
Смешанное функциональное программирование
Последнее время часто используются смешанные языки, к примеру вместе с процедурным или объектно-ориентированным программированием, что избавляет от ограничений математики, но дает гибкость в том, что функциями можно оперировать как с любыми другими типами данных, а еще дает много очень удобного сахара вроде замыканий и лямбд. Это C#, Python, JS, частично Java и C++ (в них нужны костыли в виде интерфейсов из одного метода или оберток над указателями).
Особенности
Функции как объект первого класса. К примеру в C# это реализовано через систему делегатов, которые представляют из себя тип-обертку для функций:
Action<string> printer = Console.WriteLine; // Action<string> - тип-делегат. Неявно создаем его объект и присваиваем туда функцию printer("Hello, World!"); // Вызываем функцию через делегат
Локальные функции: как обычные, только вложенные в другую функцию (объявленные внутри нее)
void Func1() { // Глобальная функция void Func2() { // Локальная функция Console.WriteLine("В локальной функции"); } Func2(); }
Лямбды: возможность объявить безымянную функцию посреди кода (часто удобнее локальных)
var pow2AsLambda = x => x * x; // => - оператор лямбда-выражения pow2AsLambda(5); // Вернет 25
Замыкания (можно использовать вместе с лямбдами и локальными функциями):
int someValue = 42; var pow2AsLambda = x => x * x + someValue; // someValue будет захвачено замыканием, хотя напрямую не передано pow2AsLambda(5); // Вернет 67
Особенность замыканий в том, что они могут захватить локальную переменную родительской функции внутрь себя, продлевая ей время жизни за пределы блока с кодом. После чего такую лямбду можно передать в другую функцию, которая просто так не имеет доступа к someValue (в обычных условиях someValue вообще уже будет уничтожен), а вот переданная лямбда сможет ее прочитать или записать всегда и откуда угодно. И самое интересное то, что значение этой переменной будет сохранятся между вызовами к замыканию. То есть она становится глобальной, но видимой только из функции-замыкания.
ООП
Посмотрим на индекс TIOBE по популярности языков. Фиолетовым я пометил чистые функциональные языки. Языки, имеющие возможности, присущие функциональным языкам (смешанные) - синим, процедурным - красным, а объектно-ориентированным (ООП) - зеленым. Языки, в которых нельзя объявить функцию, принадлежащую самой себе или модулю, процедурными считать не совсем корректно (Java).
Не претендую на 100% точность
Видите фиолетовые точки? И я не вижу. А вот синих 12. В то же время красных и зеленых по 16. При этом реально применяемый чисто процедурный язык всего 1: С. Остальные имеют какую-либо встроенную поддержку других парадигм.
И сейчас практически всё большое ПО пишут в ООП. Ибо позволяет хоть как-то сдерживать структурированность кода после перехода с десятков тысяч строк к сотням (и миллионам).
Внутри
ООП является развитием идеи процедурного программирования. Проблема была в том, что помимо кода в программе существуют еще и данные, которые было бы неплохо связать с соответствующим им кодом. И через некоторое время после появления процедур появились структуры (они же записи в Pascal, не путать со структурным программированием).
Структуры были удобным способом объединить несколько переменных в единое целое. Пример:
typedef struct user_struct { char *username; int userid; int reputation; } user_t; user_t someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.reputation += 10; // Обращаемся к полю
(Вместе с этим объявляем структуру новым типом через typedef, что не делается по умолчанию в C, в отличии от C++. Один из ярких примеров несовместимости C и C++):
После чего user_t превращается в новый тип, как int или char. Можно создать полноценную переменную типа user_t (или массив такого типа), а потом обратиться к какой-нибудь ее части (полю) через оператор точки (.). Можно сделать функцию, которая будет принимать или возвращать переменную типа user_t (к примеру someuser). Можно сделать указатель на user_t, тогда для обращение к полю используется оператор стрелки (->). Или можно встроить переменную типа одной структуры в другую, это тоже не запрещено. Внутри структурная переменная представляет из себя последовательно слепленные поля в одно целое.
Если поля разного размера, то часто добавляют пустоты между полей в целях выравнивания по размеру наибольшего поля и улучшения производительности, но это сложная тема
Возвращаемся к ООП
Суть ООП в том, что помимо полей структуры могут содержать процедуры и функции. Такая структура называется классом, переменная класса - объект, а процедура класса - метод.
Метод принципиально ничем не отличается от любой процедуры или функции, кроме того, что неявно принимает в себя специальный аргумент this (в Python это делается явно, в некоторых языках вместо this может быть self). Он содержит в себе ссылку/указатель на объект класса, от которого был вызван. Пример:
class User { public: char *username; int userid; int reputation;
void print() { printf("Пользователь \"%s\" (%i). Репутация: %i\n", this->username, this->userid, this->reputation); // Постоянно писать this-> не обязательно. Если локальной переменной с таким именем нет, то будет произведен доступ к полю } }; User someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.print(); // Вызвали метод, print() неявно получил в себя указатель this, который указывает на someuser someuser.reputation += 10;// Обращаемся к полю someuser.print(); // Теперь вывод поменяется
Да, никто не мешает объявить обычную функцию, которая будет явно принимать в себя указатель на User и ничего не поменяется, и так повсеместно делают в C (тот же WinAPI на этом построен целиком и полностью), но ООП на самом деле крайне сложная, большая и холиварная тема, которая развивалась на протяжении 60 лет в разных направлениях и которую поднимать тут глупо. Только опишу еще несколько разновидностей методов:
Статичный метод - как обычный метод, только не получает в себя this и вызывается от имени класса, а не объекта (не someuser.method(), а User::method()). Зачем это нужно и в чем отличие от просто функции? Статичный метод можно спрятать за инкапсуляцией, плюс он привязан к классу, а не болтается в глобальном пространстве имен.
Конструктор - автоматически вызывается при создании нового объекта. Может иметь аргументы, тогда обязан явно вызываться как функция с именем класса, т.е. User("IvanKr08", 17002, 253)
Деструктор - есть в C++. В C# называется финализатором и имеет несколько важных отличий. Автоматически вызывается перед уничтожением объекта (к примеру выход из области видимости, явное удаление оператором delete или удаление сборщиком мусора в C#). Не может иметь аргументов.
Свойство - есть в Delphi и C#, куда перешел от первого. С точки зрения синтаксиса это поле, которое можно читать и записывать, только вместо прямого обращения в память вызываются соответствующие методы (set и get). Пример: ... int SomeProperty{ get =>42; set => Console.WriteLine("Set"); } ... test.SomeProperty = 10; // Значение никуда не сохранится, будет выведено "Set" в консоль Console.WriteLine(test.SomeProperty); // Всегда будет 42
Перегрузка оператора - специальный метод, который позволяет переопределить стандартные операции для типа. К примеру нельзя сложить два объекта User или User и число, но если перегрузить оператор +, то можно будет определить свою логику для этого (не обязательно, чтобы оно что-то реально складывало. Это может быть любое действие).
Индексатор - синоним перегрузки оператора квадратных скобок. Объект начинает вести себя как массив.
Функтор - перегрузка оператора круглых скобок (иначе называется перегрузкой оператора вызова функции). Позволяет "вызывать" объект так же, как и функцию. Звучит запутанно, но можно погуглить. Есть в C++. Также бывает функтор в функциональном программировании, но он не имеет ничего общего с функтором в C++.
Виртуальная функция (метод) - самый интересный и сложный тип, составляет основу полиморфизма. Вкратце объяснить его невозможно, но одним из ключевых принципов ООП является наследование, то есть один класс (наследник) копирует в себя все поля и методы другого класса (родитель), и может добавить новые. И суть в том, что указатель (ссылка) на объект класса-наследника может быть присвоен в переменную-указатель родительского типа. А самое интересное то, что любые обращения (к примеру вызовы методов), сделанные через родительский указатель, будут вести себя аналогично обращениям, сделанным напрямую через указатель с типом наследника. Это называется полиформизмом. Но это было бы не слишком полезным, если бы не возможность объявить метод в родительском классе виртуальным, а в наследнике полностью переопределить его код.
И то, что я забыл
Послесловие
Если вы осилили эти 3000 слов и даже смогли что-то понять, то найдите и распечатайте себе утешительную грамоту. А я устал и пойду искать смысл жизни...
UPD: исправлены косяки форматирования, очепятки, добавлено еще пару пунктов про разновидности методов и функциональное программирование.
В первой главе мы с вами узнали, как и где создавались первые "шахматные автоматы", которые были всего лишь имитаторами программируемых шахматных роботов. Сегодня знакомство с первыми настоящими шахматными алгоритмами и программами.
*** Глава 2. Как роботы научились играть в шахматы ***
Чтение советской и американской прессы 70-х годов прошлого XX-го века оставляет приятное послевкусие.
Конечно, нельзя было говорить о дружбе между СССР и США, но отношения явно улучшились по сравнению с послевоенными годами.
В американской прессе мелькают сообщения о смягчении давления американской бюрократии на Коммунистическую партию США. В частности, в 1973 году федеральный окружной суд в Аризоне постановил, что большая часть закона против американских коммунистов неконституционна, и Аризона должна допустить КП США к участию в голосовании на всеобщих выборах ("Блавис против Болина").
В советской прессе насмешки над убогим мещанским западным (в основном американским) образом жизни продолжались (в духе Михаила Задорнова "ну, тупые"). Но при этом явно начала изменяться эмоциональная окраска этих насмешек. Злая язвительная сатира потихоньку менялась на добродушный юмор с весёлыми приколами.
Вот мы читаем сообщение о нищем, который обитает на богатой парижской помойке. Ничего особо интересного в этом факте нет. Не было никакого секрета в том, что на этом Западе нищих огромное количество, как блох на бродячей собаке. Но именно в этом нищем была интересная изюминка. На своём плакате, ниже стандартного объявления "подайте плиз, кто сколько может жертве холокоста, бюрократии и бездушия", нищий сделал странную и наглую приписку "доллары США не принимаю".
Не знаю, придумал журналист эту хохму, или реально зафиксировал нечто подобное, но эта короткая заметка порождает у читателей множество мыслей, начиная от надежд, что долларовая долговая пирамида скоро рухнет до желания дать нищему полезный совет: "бери, дурачок, что дают, потом ненужное выбросишь".
Учёные экономисты по обе стороны океана начинают осторожно высказывать идеи о возможности "конвергенции" капитализма и социализма. При этом, в США должны усилиться социальные гарантии для трудящихся, а в СССР для "деловых людей" должны быть предоставлены возможности для полезных экономических частных инициатив (типа строительства личных дач и не только).
Самое главное, что в такой атмосфере о прямом военном конфликте не могло быть и речи. Решались вопросы о возможностях сотрудничества в разных областях.
В 1972 году в Москве председатель Совета министров СССР Алексей Косыгин и президент США Ричард Никсон подписывают "Соглашение о сотрудничестве в исследовании и использовании космического пространства в мирных целях". В 1975 году в рамках этого соглашения был реализован совместный полёт советского и американского пилотируемых космических кораблей со стыковкой на орбите (знаменитый проект "Союз - Аполлон").
Вот в таких условиях мирной конкуренции и делового сотрудничества СССР и США с явным желанием обеих сторон уйти от прямых военных столкновений в 1974 году состоялось грандиозное событие для всех любителей шахмат и прикладного программирования: первый в мире чемпионат мира среди шахматных программ.
Состоялось это мероприятие в Стокгольме во время конгресса ИФИП (IFIP, International Federation for Information Processing).
Лидерами в области шахматного программирования были американцы. В США было 50 действующих шахматных программ, во всем остальном мире (Европа + СССР) около 20. Также в США уже был богатый опыт проведения внутренних чемпионатов. Последний чемпионат США стал отборочным к первому чемпионату мира. Лучшими оказались программы: "Чесс-4.0", "Теч-2", "Хаос" и "Острич". Они и представляли США на этом турнире.
Что же касается нашей страны, то у нас в боевом режиме была единственная программа "Каисса" и ещё несколько программ в стадиях подготовки и перспективной разработки. Именно "Каисса" и представляла СССР на этом турнире. По итогам турнира "Каисса" заняла первое место и завоевала золотую медаль весом 110 грамм.
После окончания турнира "Каисса" в виде бонусного трека сыграла дополнительную товарищескую партию с лучшей американской программой "Чесс-4.0". После долгой и упорной борьбы партия завершилась вничью.
Насколько сильно играли лучшие компьютерные программы в 1974 году? Мне представляется, если бы они играли в сегодняшних (2025 год) турнирах для людей, например, на популярном шахматном сайте "ЛиЧесс", то изначально показывали бы рейтинг в диапазоне 1800-2000 в блице с контролем 5+0. Для сравнения сегодняшние лучшие гроссмейстеры показывают здесь рейтинги 3000+ или, как минимум, около того. А если бы обсчитывали рейтинги современных лучших компьютерных программ типа "Стокфиш" и "АльфаЗирро" при их играх с людьми, то мы увидели бы рейтинги 4000+ или даже 5000+. Короче, эти современные роботы били бы всех людей без малейших шансов для последних. Примечание. При условии взаимной честной игры, но это уже совсем другая тема.
После первого чемпионата разработчики упорно работали над усовершенствованием своих программ.
В 1977 году состоялся второй чемпионат мира среди компьютерных программ в канадском городе Торонто.
Наша "Каисса" приняла участие и в этом чемпионате. Уже в первом туре чемпионка преподнесла неожиданный сюрприз для всех, включая зрителей, своих разработчиков и присутствовавших на турнире гроссмейстеров и мастеров.
Позиция из партии: "Duchess" - "Каисса", Торонто, 1977. Тур: 1.
"Каисса" до этого игравшая неплохо в этой чуть лучшей позиции вдруг делает ряд странных ходов, начиная отсюда: 29. … а5?
30. g4 Фe6
31. Лc6 a4?
32. Ф:а4 Лd6
33. Л:d6 Ф:d6
34. Фа8+
Здесь все ожидали естественного хода 34. … Крg7
Но "Каисса" вдруг ставит под бой ладью 34. … Лe8 и затем постепенно проигрывает без каких-либо шансов.
После партии, когда Каиссу спросили, в чем дело, она объяснила, что ход 34. ... Крg7 гораздо хуже, чем сделанный ею ход 34. ... Лe8.
В доказательство "Каисса" показала такой вариант:
34. ... Крg7 35.Фf8+! Крxf8 36. Сh6+ Сg7 37. Лc8+
"Каисса" демонстрирует вариант, которые не увидели даже гроссмейстеры
37. … Фd8 38. Л:d8+ Лe8 39. Л:e8X.
Специалисты, среди них были гроссмейстеры Ботвинник, Эдуард Ласкер, Ганс Берлинер, канадский международный мастер Леон Пиасетский эту комбинацию не обнаружили и объясняли народу этот заскок Каиссы "несовершенством шахматных программ".
В конечном результате турнира Каисса разделила 2—3 места с программой Duchess. Победила в чемпионате программа "Чесс-4.0".
До сих пор идут диспуты, а увидела бы программа Duchess во время партии этот выигрывающий ход 35.Фf8+
Далеко не факт, учитывая, ограниченность времени на обдумывание в турнирной партии.
В любом случае, если ход 34. ... Лe8 объективно сильнее (т.к. затягивал поражение на много ходов), то никто не будет спорить, что практических шансов больше у скромного хода 34. ... Крg7
Мне стало любопытно, а как бы в критической позиции пошла бы современная программа: 34. ... Лe8 или 34. ... Крg7 - ?
Я задал этот вопрос "Стокфишу" и получил ответ: 34. ... Лe8
Вот так!
Такие тонкие психологические моменты не понимали шахматные программы в 1977-м году, не понимают их они и сейчас, в 2025-м.
Возьмем этот факт на заметку, он нам пригодится для дальнейших рассуждений.
А как вообще роботы научились играть в шахматы? Точнее говоря, кто был их первым учителем?
Вообще говоря, много умнейших людей брались за эту интереснейшую проблему и решали её с разной степенью успеха.
Традиционно считается, что самым первым шахматным программистом в мире был Алан Тьюринг. В 1951 году он написал алгоритм Turochamp, с помощью которого машина могла бы играть в шахматы. Самое забавное, что это был чисто теоретический труд. У Алана не было компьютера, чтобы проверить свою программу на практике.
Тем не менее, его идеи были использованы другими учёными, а идеи тех, в свою очередь, новыми учёными, и вот, что мы имеем в сухом остатке.
Попробуем набросать примерный алгоритм игры в шахматы.
Что нам нужно сделать? Написать программу, которая умеет находить сильнейший ход в любой позиции.
Немного подумав, почитав разных полезных статей, становится ясно, что тут есть 2 принципиальных краеугольных камня.
Функция, которая оценивает позицию (оценочная функция).
Функция, которая как-то умеет определять максимальную глубину просмотра.
Давайте, попробуем набросать функцию, которая оценивает позицию.
Для простоты пока сделаем глубину просмотра равную одному полуходу и анализировать будем начальную позицию.
Примечание. Пусть будет 0.1 балл за одно свободное поле под атакой.
Премия за очередь хода. Пусть пока будет 0. Не уверен, что это вообще хорошая идея.
Проверку на мат проводим. Вообще, вряд ли кому-то будет мат в начальной позиции или после первого полухода. Но проверять надо.
Оценка позиции = Белые - Черные = (38.2+18)-(38.2+18) = 56.2-56.2 = 0
Теперь нам надо подобным образом оценить все позиции после всех возможных ходов.
Оценка позиции после 1-го хода белых
Ход Баллы
a3 0.0
a4 0.2
b3 0.2
b4 0.2
c3 0.2
c4 0.3
d3 0.6
d4 0.7
e3 0.9
e4 0.9
f3 0.0
f4 0.1
g3 0.2
g4 0.2
h3 0.0
h4 0.2
Кa3 0.1
Кc3 0.3
Кf3 0.3
Кh3 0.1
Получается следующий результат. Если применять данную оценочную функцию и использовать глубину расчета на 1 полуход, то в данной позиции получаются лучшие ходы: e3 или е4.
Теперь мы можем уточнить оценку начальной позиции. Если изначально она была равна 0, то после первого полухода стала равной 0.9.
Разумеется, если мы посмотрим чуть глубже (т.е. оценим все позиции после всех возможных 2-х полуходов), то оценка начальной позиции опять изменится. Наверное, она опять станет равной 0, например, после 1. e4 e6.
А если посмотреть немного глубже, хотя бы на 20 полуходов? Все это, конечно, можно сделать, посвятив этому процессу месяц или два, но это уже всё сделано до нас.
Не совсем i8086. Источник: http://ic.onidev.fr/map/AMD_9586A.html
Думаю, что ни для кого не секрет, что подавляющее большинство современных ПК используют архитектуру, которой скоро исполнится 50 лет. Ее современный вариант заметно отличается от того, что было в 1978 году, но при этом сохраняет практически полную двоичную совместимость (современные ПК без особого труда запускают MS-DOS, проблемы начинаются при работе с периферией). Я попытался собрать наиболее ключевые особенности, этапы эволюции и поколения архитектуры.
Вступление
1978 год. Произошло несколько политических революций, сменилось трое римских пап, открыли первый спутник Плутона, многие еще не родились, а Intel выпустили 16-битного наследника i8080: i8086, который в последствии практически полностью вытеснил другие архитектуры из потребительских ПК и стал серьезным шагом к стандартизации.
Рынок ПК на тот момент и еще в ближайший десяток лет был слабо похож на современный. Было много относительно бюджетных машин на MOS6502 (Apple I, Apple II, разные Commodore) и Z80 (ZX Spectrum), к середине/концу 80х начали появляться машины на заметно более совершенном и 16/32-битном Motorola 68K (тоже очень интересная архитектура), но общее у них было ровно одно: абсолютная несовместимость ни с кем и никак. Нет, появлялись +- совместимые между собой серии по типу Amiga или Macintosh, но они были проприетарными, а в конечном итоге загнулись (Amiga перерождалась, но в итоге умерла. Macintosh выжил только благодаря удаче и iMac, в последствии перейдя на x86 на много лет).
Причина: IBM-PC.
Про i8086
Во-первых, крайне краткий и упрощенный экскурс в работу наиболее типового процессора Фон-Неймановской архитектуры.
По факту процессор представляет из себя бешенный калькулятор, который последовательно выполняет различные операции и преобразования над числами, попутно управляя самим собой. Для этого у него есть
Многое поменялось, но суть осталась
1. Шина и память. По сути для процессора это одно и то же. Их можно представить как длинную полоску из нумерованных ячеек, способных хранить число от 0 до 255 (представьте себе швейный сантиметр), что не всегда верно (физически шина и память устроены сложно и разделены на много устройств, но при связи по шине они обычно превращаются именно в одномерную ячеистую полоску). Для чего нужна память понимают, наверное, все.
Вместе с этим там же обычно сидят периферийные устройства, к которым можно обращаться так же, как и к памяти. Пункт диапазонов памяти в диспетчере устройств Windows по сути и отвечает за то, как устройства делят эту шину между собой и памятью.
Шина состоит из шины данных, адреса и управления. Ширина шины данных это одна из ключевых характеристик, которая определяет битность процессора, еще от нее сильно зависит скорость работы с устройствами и памятью. По ней передаются, как ни странно, данные. Шина адреса представляет из себя индекс в полоске ячеек, по которому требуется произвести действие. А шина управления используется, к примеру, для выбора между чтением по адресу и записью.
2. Регистры. Маленькие именованные кусочки памяти внутри процессора. Самое быстрое и легкодоступное, что у него есть (современная разница в скорости по сравнению с памятью примерно как между взять карандаш из ящика (10 секунд) и поехать за ним в магазин (пол часа-час)). Бывают двух ключевых типов: общего назначения и особого (специального) назначения.
Первые обычно имеют размер машинного слова и используются как хранилище операндов для операций. К примеру 2 регистра могут использоваться как слагаемые, после чего в первый будет помещена сумма (особенность многих архитектур в том, что они не могут явно задействовать больше 2 регистров. Частое исключение - FMA). Ко всему прочему, регистры обычно выступают посредником при чтении/записи памяти (опять таки, зависит от архитектуры, но в некоторых вообще запрещены операции напрямую с памятью без предварительной загрузки всего в регистры, другие разрешают только один аргумент для операций брать из памяти). Таких регистров относительно мало, обычно от 4 до 32.
Название вторых крайне общее, ибо все они имеют абсолютно разные предназначения. Чаще всего встречается регистр флагов, в котором каждый бит отвечает или за состояние процессора, или за результат логической операции (для этого обычно есть инструкция CMP, которая вычитает одно число из другого (отбрасывая результат) и заносит в регистр флагов статистику: первое число было больше, меньше, равно, т.д. Потом этим может воспользоваться инструкция условного перехода). Еще есть стековые регистры и регистры, уникальные для архитектуры, но этот экскурс и так слишком длинный.
Самый важный и присутствующий везде регистр: указатель инструкции. Указывает, в какой ячейке памяти находится выполняемая инструкция. Самостоятельно увеличивается после выполнения каждой инструкции, но может быть явно перезаписан инструкцией условного или безусловного перехода на адрес (if-else в языках высокого уровня).
Если вы не закрыли пост, не уснули и за вами не приехала дурка, то продолжаем.
А теперь конкретно про i8086 и x86
20 бит шина адреса, то есть мегабайт ОЗУ, 16 бит шины данных, Фон-Неймановская архитектура, CISC, аппаратные деление и умножение, 4 16-разрядных регистра общего назначения (AX, BX, CX, DX), 8-битные регистры общего назначения, физически совмещенные с 16-битными (AL, AH, BL, BH, т.д. Делят на 2 части 16-битные регистры), 2 индексных (SI, DI. Для строковых операций), 4 сегментных (сегмент кода CS, сегмент стека SS, сегмент данных DS, дополнительный сегмент ES), 16-битный регистр флагов (FLAGS), указатель инструкции (IP). Защиты памяти (MMU) нет, полноценных механизмов многозадачности тоже.
i8088 отличался тем, что имел 8-битную шину данных и технически его можно было назвать 8-битным процессором. Это его замедляло, но зато с ним можно было построить более дешевую систему на старой 8-битной обвязке.
Сегменты
Пропущенная мною часть описания процессора, так как она присуще именно x86 по причине 20-битной шине адреса. Указатель инструкций 16-битный, все операции с памятью тоже 16-битные. Естественно, что 16-битным адресом покрыть все 20 бит адресного пространства было бы как минимум проблемно, как максимум невозможно. Но надмозговые инженеры Intel выход нашли: теперь у нас есть сегменты, а все операции с памятью локальны по отношению к ним. Это создало жуткий геморрой, особенно в высокоуровневых языках (3 типа указателей: ближние, дальние и огромные), но зато облегчило портирование старого ПО с i8080, всё адресное пространство которого влезает в 1 сегмент.
По факту сегмент представляет из себя смещение для логического адреса по отношению к физическому адресу. Значение сегментного регистра умножается на 16 (сдвигается на 4 бита) и прибавляется к логическому адресу для вычисления физического адреса, который будет выдан на шине адреса. Это приводит к тому, что у одного физического адреса появляется 16 логических "синонимов".
Если вы ничего не поняли, то это нормально. Никто не понимает, а потом приходит прозрение (и ночные кошмары). Я не знаю, как это нормально объяснить. У меня есть график, но я не уверен, будет ли он читаем и понятен
Блоки это сегменты, по горизонтали физическое адресное пространство (1 мегабайт), внутри блоков логический 16-битный адрес (64 килобайта, которые можно адресовать внутри сегмента). Вертикаль показывает наложение логических адресов на физические (те самые 16 "синонимов"). При этом в i80286 возможно переполнение и получение доступа к памяти за пределом 1 мегабайта
CISC и RISC
Это легко. CISC предлагает увеличенный набор инструкций взамен на сложность архитектуры и процессора. Время выполнения и длина инструкций может быть совершенно непредсказуемой, иногда встречаются конструкции из высокоуровневых языков (к примеру строковые операции в x86. Подсчет длины строки (strlen()) можно реализовать де-факто одной инструкцией). Удобно для написания на ассемблере, часто не очень удобно для разработчиков компиляторов. Вместе со сложностью растет энергопотребление. Это x86, i8080 и M68K.
RISC же предлагает упрощенный набор инструкций взамен их максимальной оптимизации. Все инструкции должны умещаться в строго одинаковое количество байт. Вместе с этим часто запрещено брать операнды из памяти и увеличено количество регистров. Часто запрещено обращаться к памяти без выравнивания по словам. Иногда даже нет операций деления и умножения, их приходится реализовывать программно. Типовые представители: ARM, RISC-V. MOS6502 можно в некоторой степени назвать RISC, но у него только 1 регистр и один аргумент он всегда берет из памяти (тогда так можно было делать, память была примерно равной по скорости с процессором).
Есть другие варианты, такие как VLIW или шуточные MISC, URISC и ZISC. В дикой природе не встречаются, только если VLIW у "Эльбруса".
А теперь IBM
Сюда хоть кто-то дочитал?
Как гром среди ясного неба начался 1981 год, а IBM представили свой IBM-PC, использовавший i8088. И знаете, получилось хорошо. Проблема была одна: дорого (зачем выкидывать пару зарплату на какой-то электронный гроб?). Но их покупали для бизнеса, покупали просто небедные энтузиасты, причем в больших количествах. Ожидания оправдались в 9 раз.
16-килобайтная версия стоила $1,500 (не забывайте про инфляцию, это около $5000 сейчас). Apple II с 4 килобайтами на момент выхода в 1977 стоил $1,298. Но, конечно, к моменту выхода IBM-PC Apple II успел подешеветь и нарастить память, хотя отставание в производительности было колоссальным. Но простенькие машинки Commodore были многократно дешевле и до, и после.
Amiga вышла сильно позже (1985) и в начале тоже стоила неприятно, но потом подешевела и нашла своих покупателей благодаря отличному звуку и графике. Пока IBM предлагал исключительно пищалку и ядовитый EGA (а то и малиново-голубой CGA) вплоть до 87 года за много тысяч, Amiga уже в 1985 предлагала вот такое (а еще графическую многозадачную ОС), а в 1987 делала это же за $800 в базовой комплектации. Очевидно, что покупал обычный человек себе домой, не искушенный бизнесом и работой в Excel.
А теперь главная ошибка IBM, которая их одновременно и погубила, и сделала IBM-PC стандартом: они не стали закрывать архитектуру за патентами и сделали ее крайне расширяемой. Любой мог прийти и купить за небольшую сумму всю необходимую документацию вплоть до исходников BIOS, после чего начать продавать свои платы расширения или вообще компьютеры целиком, полностью совместимые с другими IBM-PC. Причем делать это стали уже через год и очень активно. Так активно, что IBM обос... профукались и в итоге к 2006 году продали свой компьютерный бизнес от греха подальше.
Но статья у нас о поколениях процессоров, так что мы летим назад в 1982 год...
i80286
Технически существовал i80186 и i80188, но они совершенно неинтересны. Не могу сказать, чтобы и i80286 был сильно интересным.
Первое существенное отличие второго поколения x86: шина адреса теперь 24 бита, то есть 16 мегабайт. А в процессоре появился новый режим: защищенный. При этом режим, в котором работал i8086, стал называться реальным. Все последующие процессоры поддерживают все режимы предыдущих, и при этом всегда запускаются в реальном, даже спустя 50 лет. Помимо этого добавили сотню новых инструкций, в основном для работы с защищенным режимом, и нарастили производительность.
Суть в том, что в реальном режиме был коммунизм: все жили равно и ни у кого не было привилегий ограничений. Но это было крайне опасно, неудобно и мешало созданию полноценных многозадачных ОС, так как любая программа могла залезть в другую и что-нибудь ей поломать, а то и влезть и сломать ОС. Даже не обязательно специально. И на перспективе ограничение на объем ОЗУ начинало переходить из космического в потенциальную проблему недалекого будущего.
Защищенный режим на то и защищенный, что работает в связке с MMU, который позволяет разграничивать регионы памяти под разные программы и привязывать логические адреса к разным физическим адресам, тем самым позволяя реализовать виртуальную память, файлы подкачки и прочее.
Но были у защищенного режима фатальные проблемы. Ключевая: переключаться из реального в защищенный режим было легко, а вот из защищенного в реальный... ну можно было аппаратно сбросить процессор (попросив нажать пользователя на кнопку Reset). Ни о какой одновременной работе защищенного ПО со старым для реального режима речи идти не могло. Потом придумали подвести хитрую схему для программного сброса и вручную сохранять то, что будет при этом сбросе утеряно, но проблему полностью это не решало как минимум из-за серьезных тормозов и отсутствия должных механизмов для "кастрирования" от вредных привычек того, что хочет работать в реальном режиме.
Ничего хорошего из этого выйти не могло, так как всё старое ПО затачивалось под реальный режим с MS-DOS и не могло работать в ОС, использующих защищенный режим. А ОС без программ никому не нужна.
По поводу технической части мне сказать особо нечего, ибо я не работал с этим процессором и практически ничего не знаю о нем. Знаю, что осталась сегментация, но она работала абсолютно иначе и гораздо адекватнее.
I80386
Прошло 3 года. На дворе 1985 год, Intel учли свои ошибки и разработали новый вариант защищенного режима. Это самый интересный и второй по важности режим, который повсеместно использовался вплоть до конца 2000х и продолжает неявно использоваться до сих пор.
Во-первых, теперь процессор стал полностью 32-битным. Поверх старых 16-битных регистров нарастили новые регистры с префиксом 'E'. То есть теперь есть EAX, EBX, EIP, ESP, EBP, EFLAGS и так далее. Но не сегментные регистры, они остались 16-битными. Шина данных и адреса тоже стали 32-битными (шину адреса любят обрезать под лимиты конкретной платформы, но технически ее возможно было сделать 32-битной без модификаций архитектуры. В последствии разработали расширение PAE, что позволило расширить ее свыше 32 линий и 4 гигабайт).
Во-вторых, теперь появился третий (четвертый) режим: виртуальный 8086.Он совмещал в себе особенности работы реального режима и защищенность защищенного, позволяя достаточно эффективно переключаться между ними, а еще реализовывать одновременную работу множества программ реального режима внутри одной ОС одновременно с защищенными. При этом подобный псевдореальный режим оставался достаточно безопасным, так как многие опасные наглости эмулировались и не допускались напрямую к железу, а память была изолирована.
Продолжение следует
К сожалению, мысль о написании подобного текста у меня возникла слишком поздно, а на часах 4 часа 5 часов утра и я уже физически не в состоянии продолжать писать этот пост. Если это будет интересно, то я напишу продолжение, в котором полноценно расскажу про защищенный режим, костыли реального режима и пропущенный промежуток до появления длинного режима (i80486, Pentium). И так страшно представить, сколько неточностей и откровенно грубых ошибок я тут понаписывал на автомате. Если вы их нашли - просьба указать в комментариях
Обещал не делить посты на куски и такой облом, извините
Приглашаю вас окунуться в удивительный и прекрасный мир шахматного программирования. Сначала, в первой главе, чисто для разогрева, мы, с доброй улыбкой, вспомним не такое уж далёкое прошлое, когда люди пытались имитировать работу шахматных программ. Гримасы истории! Сначала игру людей выдавали за игру шахматных автоматов, сегодня игру компьютерных программ выдают за игру людей. Во второй главе вас ждёт самое начало настоящей истории шахматного программирования, включая эпические битвы советских и американских программ. В третьей главе обзор современного состояния дел на поле шахматного боя. В четвёртой заключительной главе я приглашаю посмотреть мою шахматную партию с одним из лучших на сегодня шахматных ботов. Я вам немного завидую, уважаемый читатель. Вас ждут удивительные и интересные приключения. Добро пожаловать!
В 1777 году в популярной Берлинской газете "Berliner Unsinn und Anderes" было опубликовано необычное объявление.
"Для нового популярного проекта требуется маленький смышлёный мальчик примерно десяти лет. Работа лёгкая, лежачая. Мальчик должен быть спокоен, молчалив и хорошо знать математику. Желательно еврейской национальности. Оплата по договоренности. Бесплатное питание гарантируется в любом случае".
- Марта, дорогая! Мы спасены! - закричал Ганс Липке своей жене, прочитав это объявление, - один из наших десяти детей теперь не будет голодать! Иди сюда!
Жена Ганса Марта немедленно бросила стирку белья, вытерла руки о подол и надела очки.
- Ну и каким жуликам ты поверил на этот раз? - спросила Марта, начиная чтение, - где читать? Вот это что ли, "новый способ быстро разбогатеть?"
- Нет, ниже, там, где про "лежачего мальчика". Наверняка, это какой-то пансионат для особо одарённых детей евреев типа нашего Питера.
- А разве наш Питер еврей? - удивилась Марта.
- Тебе видней, ты ведь его рожала. Да это и неважно! Скажем, что еврей, как они узнают, так это или нет. Не будут же они измерять штангенциркулем его череп. Вообще, Питер по всем параметрам подходит. Маленький, постоянно лежит и молчит.
- А разве он знает математику?
- Сейчас проверим. Эй, Питер! Сколько будет дважды два?
- Четыре, - ответил Питер после минутного размышления.
- Гений! Вставай и собирайся. Пойдём на работу. Какая тебе разница, где лежать и молчать? А там тебя за это будут кормить.
Работодатель, некий Вондерганг фон Шахритер объяснил суть работы.
Имеется ящик из дорогого дерева. На этом ящике расположена шахматная доска с фигурами. Мальчик должен быть размещён лёжа внутрь ящика прямо под шахматную доску. Специальный механизм позволял мальчику чётко видеть движения фигур, а также передвигать их.
- Очень перспективная тема сейчас в Германии, да и вообще в Европе, - с энтузиазмом рассказывал Шахритер, - интерес народа к автоматизации шахматной игры просто зашкаливает. Несколько десятков шахматных автоматов ползают по нашей стране, огромные деньги заколачивают. Да, чуть не забыл. Ты, паренёк, хорошо в шахматы играешь?
Питер впал в глубокое раздумье.
- Задам вопрос попроще, - понимающе кивнул головой Шахритер, - ты в шахматы играть умеешь?
На этот вопрос Питер ответил довольно быстро:
- Не знаю, не пробовал.
- Не слушайте его! - закричал Ганс Липке, - он очень талантливый. Задачки по математике щёлкает как орехи. А любой математик - отличный шахматист, это всем известно.
- Ладно, - махнул рукой Шахритер, - я его научу. Шахматы - это просто. Вот бином Ньютона - это сложно. Питер, ты сам-то хочешь этим заниматься? Будешь работать шахматным автоматом?
- Буду, - ответил Питер неуверенно, но зато быстро.
- Попробовал бы ты только отказаться, - проворчал Ганс Липке, - я бы тебе сделал ход конём по голове, чтобы лучше соображал, чего ты хочешь делать, и чего ты будешь делать.
Так началась карьера Питера, одного из многих шахматных автоматов конца 18-го, начала 19-го веков. Несмотря на то, что "шахматы - это просто" по утверждению Шахритера и упорные тренировки, играл в шахматы Питер плохо. Зато он проявил себя в роли артиста. Когда он проигрывал, он начинал очень правдоподобно рыдать басом. Когда выигрывал (случилось и такое), очень язвительно обращался к сопернику, "вам матец, херр Миттельбрахенветтер, ха-ха-ха".
Народу нравились такие шоу. Нравилась даже относительно слабая игра Питера. Ведь у него мог выиграть даже слабый игрок, а это положительно сказывалось на интересе к автомату Питера. Какой интерес играть с автоматом, который всё время выигрывает?
Шахритер придумал хорошую систему ведения бизнеса. Партия игралась на довольно большую ставку, а также продавались недорогие билеты на просмотр игры. В результате, даже если Питер проигрывал, убыток с лихвой покрывался за счёт зрителей.
К тому же через пару лет, благодаря приобретённому опыту, качество игры Питера заметно улучшилось, что положительно сказывалось на доходах.
Беда подошла с другой стороны. Питер подрос и перестал помещаться в ящик. Можно, конечно, было заменить ящик на другой, больший по размеру.
Но хитрый Шахритер подошёл к решению проблемы с другой стороны. Он просто нашёл другого мальчика, более молодого, подходящего по габаритам. Питер перешёл на должность тренера, консультанта и ответственного за безопасность.
Так дело двигалось и дальше. Постепенно команда Шахритера увеличилась до десяти человек, работа находилась для всех. Для самых способных парней всё-таки делали новые ящики для игры. Другие работали на должностях "подай - поднеси". Ну, чисто как "Ласковый Май"!
Самым крупным успехом корпорации была партия одного из автоматов против самого Наполеона. В шахматы Наполеон играл плохо, но у автомата хватило ума эту партию проиграть. Это было правильное решение. Наполеон очень нервничал, когда проигрывал. В приступе ярости он вполне мог разгромить автомат и поколотить членов команды, тусовавшихся рядом.
Неплохо шли дела у Шахритера и его команды! Но только некоторое время.
Всё хорошее когда-нибудь кончается. Начались разоблачения конкурирующих автоматов, а через это у народа начал пропадать интерес к шахматной игре всех остальных профессионалов рынка. Народ желал играть именно с автоматами, а не с какими-то малогабаритными жуликами спрятанными внутрь.
Доходы сначала упали, а затем и вообще пропали.
Шахритер уехал в Америку в поисках новых идей, а участники его команды разошлись кто куда.
Вот такие гримасы истории! Если сейчас подозреваются люди в том, что они используют подсказки шахматных движков, то тогда подозревались автоматы в том, что за них думают люди.
Прошла эпоха шахматных автоматов со встроенными в них живыми людьми.
Началась эпоха настоящих шахматных автоматов, которые играют вполне самостоятельно.