После несерьёзной статьи на серьёзную тему Job Safety Driven Development возникла идея написать о том, как появляются ошибки разработчиков. Вместо этого появилась статья «Почему всё ломается даже у хороших программистов?» (Часть 1 и Часть 2). Мысль нужно закончить. Уже рассмотрено два краевых случая, давайте посмотрим и на «обычные» причины ошибок программистов. В первой части этой статьи мы рассмотрели, что такое ошибка вообще, цену и ценность ошибок, немного коснулись темы интерактивного программирования и разработки через багфикс. На примере "ошибки выжившего" поняли, как формируются негативные паттерны поведения. Здесь же мы рассмотрим проблему оптимизации, которая по своей разрушительной силе не очень уступает разработке через багфикс. Посмотрим, а нужны ли программисту фундаментальные знания, а если да, то зачем. И попытаемся затронуть проблему слепого копирования ритуалов (карго-культ) в программировании. Продолжаю пробовать писать простым языком, чтобы было понятно широкой аудитории.
Меня зовут Константин Митин, я сооснователь и руководитель компании АйТи Мегастар/АйТи Мегагруп. Когда-то был простым разработчиком, работал в L3, дорос до тимлида, затем и до руководителя филиала разработки крупной ИТ-компании. Теперь я в АйТи Мегагруп.
В одном ряду с разработкой через багфикс стоит ещё проблема оптимизации быстродействия и потребления ресурсов. Если из-за разработки через багфикс вы рискуете выйти за бюджеты и просто не завершить проект, то проблема оптимизации действует более коварно. С одной стороны, по полной аналогией с разработкой через багфикс, вы можете просто не завершить проект, так как выйдут все бюджеты и разумные сроки. А с другой стороны, можете выйти на продуктив и умереть, как бизнес, от того, что ваш продукт будет слишком медленный и слишком много потреблять ресурсов. Давайте поймём, почему такое может происходить.
Проблема оптимизации скорости и потребления памяти
Ещё один важный момент, который нельзя обойти стороной, - быстродействие и ресурсоёмкость выполнения разработанного кода. Приложение может корректно отрабатывать все необходимые позитивные и негативные сценарии, но из-за ограничений своего быстродействия и потребления ресурсов быть совершенно немасштабируемым либо непригодным к эксплуатации.
Вас когда-нибудь интересовало, почему со временем ваш телефон либо компьютер работает все медленнее и медленнее? И в какой-то момент технику приходится менять, чтобы выполнять те же самые задачи, с которыми она ещё так недавно прекрасно справлялась? Этот интересный вопрос иногда звучит от пользователей.
Часть ИТ-стартапов гибнет из-за того, что стоимость аренды вычислительных ресурсов становится непомерной для их финансовых моделей. Даже в крупных компаниях бывает такое, что руководство внезапно для себя обнаруживает здоровенный ЦОД (центр обработки данных) за сколько-то миллиардов денег, а им всё равно продолжают говорить, что вычислительных мощностей мало. То есть имеет место проблема расточительного и неоптимального использования вычислительных ресурсов.
На самом деле, я уже частично касался её в своей статье «Почему всё ломается даже у хороших программистов?» (Часть 1 и Часть 2), когда описывал свой личный опыт. В какой-то момент оказалось, что я не могу получить из базы данных фрагмент данных в 64 мегабайта потому, что внутри кода платформы бэкенда объём затрачиваемых данных начинал превышать 2 гигабайта. Для компании это всё выливалось в заметные денежные затраты на вычислительные мощности и на работу программистов, которые были вынуждены работать на такой платформе.
Проблема адекватной производительности программного кода имеет большое значение. Почему я пишу именно об «адекватной» производительности? Если производительность будет слишком низкой, то будут быстро расти затраты на вычислительные ресурсы и в какой-то момент эти затраты могут стать непосильными для бизнеса. С другой стороны, на разработку высокоэффективного кода нужно тратить гораздо больше времени программиста. В какой-то момент такие затраты начнут стоить больше, чем стоимость аренды вычислительных ресурсов.
Преждевременная оптимизация - это тоже плохо.
С учётом написанного выше, в сообществе разработчиков сформировалась в целом верная мысль, что сначала код должен заработать и пройти приёмочные тесты, потом уже можно заниматься его оптимизацией. Писать сразу оптимальный код действительно дороже. То есть стоимость ошибки при разработке начинает расти. Поэтому этап оптимизации вынесли на стадию отладки, когда уже есть какой-то рабочий код, опираясь на который можно уже написать рабочий и оптимальный код.
Желательно изначально понимать, насколько следует оптимизировать код по скорости выполнения и потреблению памяти. И воспринимать недостаточную производительность кода как ошибку.
Схожие подходы нужно использовать и при написании MVP (minimum viable product - минимально жизнеспособный продукт). На этой стадии реализуется продукт для проверки гипотезы, что он вообще нужен рынку. Делать его оптимальным по быстродействию — терять больше ресурсов в случае ошибки. Делать его совсем неоптимальным — закрывать себе возможность быстрого изменения продукта.
Как только ваш MVP будет иметь успех на рынке, это заметят ваши конкуренты, которые начнут разработку своих продуктов. Если ваш MVP будет раздражать пользователей своими ошибками, очень медленно работать и медленно развиваться (о расширяемой архитектуре же никто не подумал?), то ваши конкуренты скоро нагонят и перегонят вас.
Как только ваш MVP будет иметь успех на рынке, это заметят ваши конкуренты, которые начнут разработку своих продуктов.
Нужно помнить, что MVP - это не что-то дешёвое и низкокачественное, удешевление идёт прежде всего за счёт реализации только минимально необходимого функционала. И сам MVP должен быть с заранее заданным качеством и быстродействием, стоит заранее подумать, каким он должен быть.
Но мы всё же о негативном паттерне поведения разработчиков. Как, собственно, сложился этот паттерн? Аналогично с разработкой через багфикс соединилось несколько обстоятельств:
Идея о том, что сначала нужно написать работающий код, а потом заниматься оптимизацией.
Наблюдение о том, что вычислительные ресурсы стоят меньше, чем время разработчика.
И действительно, мощность вычислительных ресурсов монотонно растёт, цена за них падает. Иногда стоимость оптимизации кода выше, чем просто докупить ещё ресурсов. Но только пока вы не начнёте делать нечто большое и сложное.
Кроме затрат на дополнительное время разработчика, которое потребуется, чтобы оптимизировать код, есть ещё одна проблема. Эта проблема — квалификация разработчика. Чем более высокий уровень быстродействия нужно достигнуть, тем выше должна быть квалификация разработчика. Кроме того, могут потребоваться глубокие фундаментальные знания. И какую-то работу смогут выполнять уже не просто программисты, а только прикладные математики, иногда с учёными степенями.
Чем более высокий уровень быстродействия нужно достигнуть, тем выше должна быть квалификация разработчика.
Недостаточность фундаментальных знаний
Воспользуюсь случаем и скажу спасибо тем людям, которые незаметно, но весомо сделали существенный вклад в то, что я состоялся, как программист. Это мой учитель информатики в школе и множество преподавателей в университете.
Скорее всего, хороший программист ещё в школе интересовался программированием на уроках информатики. Хотя повезло так далеко не всем. Мне компьютер был доступен с первого класса, но у меня в университете были одногруппники, которые приехали из деревни и впервые увидели компьютер в терминальном классе. Конечно, там уже стояли обычные компьютеры, терминалы и мэйнфреймы на тот момент в университете не применялись.
Очень хорошо, когда учитель информатики в школе не мешает своим ученикам развиваться самостоятельно. То есть даёт то, что может дать, а потом лишь направляет свободный поиск своего ученика. Именно в это время закладывается любовь к программированию. Чтобы стать успешным программистом, программирование нужно именно любить. Это нужно запомнить, потом пригодится.
В школе меня научили, смотря на программный код, написанный на доске, вслух и по шагам произносить, как он будет выполняться. Чудесный навык. Если вы не можете этого сделать, вы очень быстро нащупаете свой потолок в программировании.
В университете мне рассказали, как работает центральный процессор компьютера. Рассказали про уровни кеширования в процессоре, оперативной памяти и виртуальной памяти. Объяснили, почему, если это не учитывать, код будет работать нестабильно и медленно. Показали, как хранится число с плавающей точкой в памяти компьютера, и почему с ними нельзя работать, как с целыми числами. И ещё очень много всего разного, что обычно люди упускают.
Если вы считаете это неважным, то вам стоит встретиться с бухгалтером, который не может сдать годовой отчёт, потому что он на 1 копейку не сошёлся из-за ошибок округления. В момент, когда до срока окончания приёма отчетов остался один день, а дальше уже пойдут вопросы и штрафы от налоговой.
Иногда люди спрашивают, а зачем это всё? Тут всё просто, "это всё" уберегает от наивных ошибок, после которых код может начать работать на порядки медленнее и потреблять на порядки больше памяти.
Реальный уровень человека чувствуется сразу же, когда нужно использовать конструкцию типа array.append() в цикле раз так сто тысяч. Либо человек понимает, что для добавления нового элемента в массив из N элементов нужно:
выделить память в N+1 элементов;
скопировать N значений из старого массива в новый массив;
добавить в новый массив N+1 элемент;
освободить память из N элементов старого массива.
Либо человек не понимает, сколько памяти и времени займёт одна строчка его кода, тем более явно либо неявно (например, мы вызываем функцию) запущенная в цикле.
Можно возразить, что в современных библиотеках это всё учтено, и у массивов есть буфер под увеличение размера. Но ведь им нужно же ещё уметь управлять, а для этого нужно не только знать, что он там есть, но и понимать, почему он там появился.
Смысл всех упражнений с низкоуровневым программированием, изучением алгоритмов сортировок, обходов графов, написанием собственного компилятора и прочего во время обучения - дать человеку возможность понимать, что он собирается сделать, сколько памяти и операций на это может потребоваться.
Если вернуться к теме оптимизации кода, то окажется, что разработчик с хорошей базой многие вещи делает инстинктивно и не задумываясь над этим. Перерасход ресурсов и утечка памяти для него такая же ошибка, как и неверный результат выполнения кода.
Сегодня мы видим обширную работу над тем, чтобы снизить уровень входа в профессию разработчика. Для этого придумывают новые инструменты, вводят дополнительные уровни абстракции (на которых часто падает производительность), разрабатываются ускоренные курсы обучения и переквалификации.
Однако всё же есть разница между человеком с университетским образованием в области компьютерных наук и человеком, который окончил курсы повышения квалификации длиной максимум год. Беда в том, что в университетской программе обучения обычно присутствует большой объём математических дисциплин, которые ставят мышление человеку. Какую-то базу можно наверстать самостоятельно, но с постановкой мышления так не выйдет.
Но стоит отметить, что физики, радиотехники и ещё ряд технических специальностей неплохо переквалифицируются в программистов. Мышление во время обучения им тоже ставят.
Тем не менее, ещё лет 10 назад мощности смартфона с избытком хватало на управление спутниковой группировкой СССР. Сегодня этой мощности не хватает ни на что.
Другой пример уже из моего опыта. Когда-то я работал в компании Тензор (облако СБиС), для которой ограниченность вычислительных ресурсов их ЦОДа (стоимостью сколько-то миллиардов рублей) была проблемой. Благодаря работе Кирилла Боровикова, который взял под свою ответственность эффективность работы с базами данных, многие SQL-запросы начали работать в тысячи раз быстрее и потреблять во много раз меньше оперативной и дисковой памяти.
Это было сложной работой. Ему приходилось массово учить людей писать оптимальные запросы, когда в компании несколько тысяч программистов, это не так просто. В компании действовала система отслеживания неоптимальных запросов, которые исправлялись, как ошибка реализации. Но благодаря этой большой и сложной работе компания экономила очень большое количество денег (десятки, может быть сотни миллионов рублей) на своём ЦОДе.
Однако работа с реляционными базами — это чистая математика. Нужна база, нужно поставленное мышление.
Если вы хотите научиться оптимально работать с PostgreSQL, то вам сюда.
Слепое следование ритуалам без понимания их сути
Негативных паттернов поведения в программировании осталось ещё очень много, а вот места в статье нет. Рассмотреть каждый по отдельности не получится. Но многие из них объединяет именно слепое следование ритуалам без понимания их сути. Как такое получается? Давайте рассмотрим на простом примере «код-ревью».
Опыт «программирования вообще» очень сильно отличается от опыта коммерческого программирования. Можно создать домашний либо учебный проект впечатляющих масштабов и сложности, но остаться на уровне стажёра. С научным кодом где-то так же. Умение писать научный код не конвертируется автоматически в умение писать коммерческий код.
Прежде всего так получается из-за того, что работать приходится не в одиночку и не для себя. Вокруг есть другие разработчики, на которых могут повлиять результаты твоей работы. Есть заказчик и множество людей за заказчиком, которые непосредственно столкнутся с результатами твоей работы. Это новые обстоятельства, их нельзя не учитывать. Иначе они просто придут и напомнят о себе, как соседи в общежитии.
Для профессионального развития программисту полезно на момент стажировки поработать с хорошим наставником. У наставника должен быть богатый опыт коммерческой разработки, наставник должен понимать, что и зачем он делает, кроме того, наставник должен понимать, чего делать не надо и что будет, если не сделать той либо иной важной вещи.
Наставник должен передавать понимание о важности того либо иного ритуала и причин его возникновения. Один из таких ритуалов является «код-ревью», либо инспекция/рецензирование кода одного разработчика другим разработчиком. Рецензирование кода придумали для того, чтобы более опытные разработчики могли учить менее опытных разработчиков до того, как их код сможет что-то сломать на стенде разработки либо на тестовом стенде.
Для профессионального развития программисту полезно на момент стажировки поработать с хорошим наставником.
Важен не сам факт обнаружения ошибки в коде либо отклонения от принятых в компании правил, важно объяснение, почему так делать нельзя. Основная ценность рецензирования кода — это передача опыта. В норме человек, который проводит инспекцию кода, опытнее, чем человек, инспекцию кода которого проводят.
Давайте представим, что у нас в компании работает три джуна, которые делают друг другу «код-ревью». Никто из них даже не подозревает, а как правильно. Максимум, что они могут сделать, так это попробовать следовать популярным (в интернете) «бест практис», которые они ещё могут неправильно понять. Таким образом, дело даже не в 0-й пользе, не в увеличившейся стоимости разработки (инспекция кода — не бесплатная процедура), чаще всего таким образом ещё и вред наносят.
Кроме того, нельзя забывать о контексте, в котором будет выполняться код. Мы живём в эру интерактивного программирования. Гораздо легче запустить код и посмотреть, как он работает, чем изображать из себя статический анализатор кода, который должен выявить «состояние гонки, утечку памяти и выход за границы массива». Современные среды разработки уже делают это всё лучше среднестатистического разработчика. Да, некоторые сеньоры на глаз могут увидеть такие ошибки в коде, но далеко не все.
Инспекция кода — не бесплатная процедура.
Нельзя забывать и о контексте задачи, который решает код. То есть желательно знать, что вообще это за код, каков должен быть результат его выполнения, и хотя бы тот модуль, в который этот код вносится. Без понимания всего контекста проверка будет формальной. А на понимание контекста нужно потратить очень много времени.
Иногда говорят о том, что это помогает людям лучше знать и понимать код друг друга. Но разработчик может и свой код через месяц забыть, не говоря уж о коде кого-то ещё, который он видел только по коммитам.
Ещё рецензирование кода применяют, чтобы разработчики не нарушали принятых в компании соглашений о кодировании. Это не только «код-стайл», это могут быть, например, правила работы с базами данных либо ещё что-то. То есть «код-ревью» появляется из-за недоверия к своим разработчикам. Но здесь лучше устранять причину проблемы, а не развивать контролирующие механизмы.
Итак, у нас есть три джуна, которые делают «код-ревью» друг другу. Смогут ли они выступить наставниками друг для друга? Конечно, нет. Смогут ли они уберечь друг друга от нарушения принятых правил? Возможно, но, скорее всего, нет. Просто сговорятся друг с другом.
А если это будет не три джуна, а три мидла? Сработают ровно те же ограничения. И с тремя сеньорами — тоже.
Однако, если у вас много разработчиков, вам постоянно нужно пополнять команду, и вы это делаете через джунов, то «код-ревью» очень хороший метод. Джунам «код-ревью» делают мидлы, мидлам «код-ревью» делают сеньоры, сеньорам «код-ревью» делает техлид. Как результат, работает каскадная система передачи знаний и быстрого обучения. Кроме того, начинает работать принцип: «Обучая — учимся».
Примеров неверного применения тех либо иных практик очень много.
Примеров неверного применения тех либо иных практик очень много. Мне кажется, что и желающих рассказать, почему они могут не работать, и как сделать, чтобы они заработали, тоже не мало. Например, есть Agile-коучи, которые рассказывают, как правильно должны работать те либо иные Agile-практики. Слепое копирование работает плохо.
Подводя итоги
Когда мы говорим об ошибках программиста, стоит помнить, что это не что-то страшное, чего стоит избегать всеми силами. Кроме того, процесс интерактивного программирования — это такой же процесс проверки гипотез через пробы и ошибки, как в маркетинге, бизнесе и других областях. Однако, он хорошо автоматизирован и происходит очень быстро. Но заключительной фазой разработки кода до того, как он уйдёт на тестирование, должна быть отладка, после которой все основные сценарии работы должны функционировать, краевые негативные кейсы проверяться. Задача QC — искать нетривиальные баги, а не участвовать в разработке через багфикс.
Другой значимой проблемой является оптимизации быстродействия и потребления ресурсов. С одной стороны из-за преждевременной оптимизации вы можете просто не завершить проект, так как выйдут все бюджеты и разумные сроки. А с другой стороны, можете выйти на продуктив и умереть, как бизнес, от того, что ваш продукт будет слишком медленный и слишком много потреблять ресурсов. За проблемой оптимизации часто прячется проблема недостатка фундаментальных знаний.
Многие проблемы в программировании проистекают из слепого копирования ритуалов и правил без понимания их сути, контекста и границ применимости. То, что было правильным либо неправильным для других, не факт, что подойдёт для ваших условий.
Очень сложно научиться чему-то на негативном опыте, ведь путей решения проблемы намного меньше, чем путей, как не справиться с задачей. Тем не менее, на чужие ошибки нужно смотреть и запоминать, где и какие могут быть подводные камни. Тогда, анализируя чужой успешный опыт, вы сможете понять, за счёт чего достигается успех и как обходятся подводные камни. Только так вы сможете адаптировать чужой опыт под свои уникальные условия.
Если вы дочитали до конца и что-то для себя поняли, то спасибо вам.
части. Как всегда, попробую писать простым языком, понятным широкой аудитории.
Комментарии (13)
ReadOnlySadUser
06.07.2022 08:52+2Чтобы стать успешным программистом, программирование нужно именно любить.
...первые пару лет. Потом вообще пофигу, любишь ты или нет, основной навык уже усвоен, а развиваться дальше можно испытывая абсолютный пофигизм к программированию или даже слегка его ненавидя. Личный опыт)
ReadOnlySadUser
06.07.2022 08:57+2В компании действовала система отслеживания неоптимальных запросов, которые исправлялись, как ошибка реализации
Немного наивный вопрос - если существует автоматизированная система по отслеживанию неоптимальных запросов, быть может стоило сделать сдедующий шаг и сделать из неё оптимизатор, а не заставлять тысячи программистов переписывать с понятного на оптимальный?)
constantine_mitin Автор
06.07.2022 09:22Тут все не так просто. Кроме времени выполнения запроса там логировался даже план выполнения запроса. В зависимости от плана выполнения запроса очень сильно меняется время его выполнения. Обычно есть несколько разных способов записать в целом один и тот же запрос, на больших и сложных данных время выполнения разных форм одного и того же запроса могут отличаться в сотни раз. Потому, что планировщик и оптимизатор PostgreSQL штука умная, сложная, учитывающая статистические данные, но не всесильная. Иногда план запроса не попадает в нужные индексы, иногда нужного индекса просто нет. Иногда все ломается (оптимальный план запроса) от простого джойна.
То есть здесь именно тот случай, когда машинная оптимизация не справилась и нужно человеческое участие. На самом деле, PostgreSQL здесь не уникален. Похожим образом может происходить оптимизация запросов для MS SQL, Oracle и других реляционных баз данных.
ReadOnlySadUser
06.07.2022 10:26Я скорее имел ввиду, что если у вас была автоматизированная система по обнаружению того, что разработчик написал неоптимальный запрос, а также разметка этого запроса как "ошибка реализации", то очевидно система знала, что именно в этом запросе не так? Или я неправильно понимаю логику работы таких анализаторов?
constantine_mitin Автор
06.07.2022 11:28Понять, что с запросом происходит что-то не то на продуктивных данных, можно только в момент его выполнения. Поэтому мы сейчас говорим о системе логирования времени выполнения запросов и планов их выполнения. И системе мониторинга самых долгих и тяжёлых запросов. Планы же запросов нужны, чтобы понимать, что пошло не так. Не попали в индекс, спровоцировали sec scan либо ещё что-то подобное.
ReadOnlySadUser
06.07.2022 11:34+1Окей, тогда это не "система отслеживания неоптимальных запросов", а просто обычно логирования запросов, которая рассылает письма счастья тем программистам, которые пишут самые отстойные (долгие) запросы?
qw1
06.07.2022 10:20+1Оптимизацию не всегда можно сделать на уровне одного запроса.
Часто надо рассматривать все SQL-запросы от одной операции как целое, например, классическая N+1 проблема не лечится оптимизацией какого-то одного запроса, а требует переписывания модуля (со стороны SQL-сервера мы даже не знаем, на чём написан клиент — java? python? и поэтому не знаем, как автоматически переписать).constantine_mitin Автор
06.07.2022 11:37Это действительно так, но реализовать это придётся уже не на уровне PostgreSQL. Хотя есть такая вещь, как транзакция, и можно смотреть общее время её выполнения. Долгими они тоже не должны быть, чтобы не начать ловить дедлоки из-за блокировок на чтение и запись.
Кроме того, информацией о транзакцией обладает сервер с бизнес-логикой (написан на Python и C++). Этот сервер предоставляет API, время которого тоже можно отслеживать. Такие рейтинги тоже были, но контролировались не так плотно.
Ну и частота вызова метода API и SQL-запроса тоже имеет значение. Чем чаще вызывается, тем больше может быть потребность в оптимизации. Суммарное время и потребление памяти тоже отслеживалось.
funca
06.07.2022 10:31Немного наивный вопрос - если существует автоматизированная система по отслеживанию неоптимальных запросов, быть может стоило сделать сдедующий шаг и сделать из неё оптимизатор, а не заставлять тысячи программистов переписывать с понятного на оптимальный?)
Выглядит как маленький управленческий шаг для руководителя, и огромный для всего человечества). Когда-нибудь именно так и будет, и автоматические оптимизаторы будут разгребать горы легаси, превращая код в оптимальный. И тысячи программистов останутся без работы. Но пока задачка из области фантастики, и ни в какой бюджет - даже самый фантастический.
funca
06.07.2022 10:14Приложение может корректно отрабатывать все необходимые позитивные и негативные сценарии, но из-за ограничений своего быстродействия и потребления ресурсов быть совершенно немасштабируемым либо непригодным к эксплуатации.
Когда вам нужно построить дом, вы кого будете искать? Ежу понятно - строителей (ну не экономистов же, юристов, или там архитекторов). А если софт? Разумеется - тыжпрограммистов. ;)
Функциональные требования определяют "что" приложение должно делать. Но ведь одну и ту же функцию можно написать десятками разных способов?
"Как" приложение должно работать, в большей степени зависит от нефункциональных требований (performance, usability, reliability, maintainability, testability, regulatory и прочие *ty). Они же в основном и _определяют архитектуру_.
Дизайн архитектуры и программирование это два довольно разных аспекта. Много ошибок возникает на стыке этих двух областей, в том числе и чисто управленческих.
Состав и уровень команды определяет уровень сложности проекта, который они смогут вывезти в отведенное время. Функциональный прототип PoC зачастую могут собрать и программисты, MVP уже стоит проектировать, привлекая специалистов из разных областей.
saipr
06.07.2022 10:25Ещё один важный момент, который нельзя обойти стороной, — быстродействие и ресурсоёмкость выполнения разработанного кода.
О, как владели приёмами оптимизации патриархи отечественного программирования. Как мы, курсанты первого набора программистов в военной академии им. Ф.Э. Дзержинского, восхищались детищем дважды Лауреата Государственной премии СССР М.Р. Шуры-Буры системой обслуживания библиотек стандартных подпрограмм ИС-2 (Интерпретирующая Система). В ИС-2 было всё — и наука, и искусство:
Интерпретирующая система ИС-2 (позднее ИС-22) представляла собой маленький шедевр как по технологии наполнения библиотеки СП, так и по простоте ее использования, по минимуму накладных расходов.
Я сам писал транслятор РПГ-М-220, имея в своем распоряжении 4K оперативной памяти, иагнитный барабан на 28K и восемь лентопротяжек:
ne555
Иногда и наоборот, когда ракеты падают.
constantine_mitin Автор
Очень интересный случай, спасибо.