Спартанское обучение нейронных сетей

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

image

Если кто-то хочет лучше понимать, о чем будет речь ниже, то можно прочитать эти статьи на хабре: статья1 и статья2

( далее добавление шума будем называть словом мутация, не потому что модно, а потому что в одно слово, а все зашумленные сети являются потомками родителя )

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

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

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

Общая идея:

Инициализируем либо загружаем веса модели-родителя

  1. Обучаем N эпох модель-родителя
  2. Создаем K потомков разными мутациями
  3. Считаем фитнес-функцию по потомкам
  4. Объявляем нового родителя (об этом будет каждый эксперимент)
  5. goto 1

Общие детали эксперимента:

Датасет — CIFAR10
Модели — resnet18 с нуля и предобученная
Оптимизатор — SGD
Функция потери — CrossEntropyLoss
Функция оценки качества модели и ее потомков — accuracy
Мутации каждые 5 эпох
Обучение шло 50 эпох, эволюция засчитывалась за эпоху
На каждую мутацию 40-50 потомков

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

Эксперимент №1. Метод случайных градиентов.

А в процессе того пока мы над этим работали это стало напоминать метод отжига.

Похожие штуки пробовали в статьях: Adding Gradient Noise Improves Learning for Very Deep Networks, Evolutionary Stochastic Gradient Descent for Optimization of Deep Neural Networks.

For i in range(N):    Обучаем N эпох модель-родителя     Градиентным методом  с SGD    # часть с мутациями    For k in range(K):       Создаем потомка добавлением шума       Если он оказался лучше родителя делаем его родителем.

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

А теперь Gо в детали.

Чтобы все заработало нужно было решить следующие вопросы:

  1. Было непонятно как выбирать лучшего потомка, по целевой метрике или по лоссу, ведь он тоже менялся.
  2. Как видно ниже (Картинка 1) улучшения не такие хорошие как хотелось бы. И в то время как скор за счет шума растет на трейне он каждый раз чуть — чуть падает на валидации. Делаем вывод что метод не работает?
  3. Просто добавление шума самом собой не заработало и пришлось много шаманить.

Сейчас я подробно расскажу про эти три пункта.

image
Картинка 1. Выбор лучшего потомка по accuracy.

По какой метрике оценивать, по лоссу или по целевой. Мы попробовали и по лоссу и по целевой метрике accuracy в нашем случае, в результате, всегда работает только по лоссу. Вот еще пример где лучшего потомка выбирали по лоссу. (Картинка 2).

image
Картинка 2. Выбор лучшего потомка по лоссу.

После мутаций метрика на валидации становится хуже.
Получалось что после каждого этапа мутаций скор падал на валидации? Делаем вывод что ничего не работает? Как оказалось все не так просто. При этом скор чуть чуть быстрее рос при проходе SGD на мутировавших (зашумленных) потомках. В итоге финальный скор был чуть лучше. Это было удивительно, приятно и абсолютно не понятно.

Буквально чуть лучше:
Accuracy 47.81% — наш велосипед с шумом.
Accuracy 47.72% — просто SGD.

Тут надо пояснить что этап с мутациями мы считали как одну эпоху. Мы делали 40 мутаций каждую пятую эпоху. Поэтому общее число итераций у SGD больше. Но при этом финальный скор все равно лучше.

image
Таблица 1. Метрика Аccuracy, resnet18, CIFAR10 на 10 классов, SGD. 40 мутаций каждую 5ю эпоху. Выбор лучшего потомка по лоссу. Серый график обучение SGD, синий график наш метод.
image
:—:
Таблица 2. Метрика Аccuracy, предобученный resnet18, CIFAR10 на 10 классов, SGD. 40 мутаций каждую 5ю эпоху. Выбор лучшего потомка по accuracy. Розовый график обучение SGD, оранжевый график наш метод.

В такой постановке мы сделали 4 эксперимента, предобученый и не предобученный resnet18 выбор потомка по лоссу и по accuracy. Улучшений не было на не предобученном при выборе потомка по accuracy. В остальных случаях было чуть чуть лучше.

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

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

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

Собственно поэтому этот велосипед мы и назвали метод случайных градиентов.

Плюсы:

  1. Итераций меньше, а скор чуть лучше.
  2. Это работает как регуляризация для вала. Во всех случаях скор на трейне у нас был всегда хуже, а на вале в итоге почти всегда лучше.

Минусы

  1. Много танцев с бубном ради очень маленького прироста. Появляется еще куча гипер параметров которые надо подбирать.
  2. Для шума нужно делать backward.

Эксперимент №2. Эволюционные стратегии

Для мутаций я использовал алгоритм описанный OpenAI в работе Evolution Strategies as a Scalable Alternative to Reinforcement Learning, реализация алгоритма была взята из репозитория https://github.com/staturecrane/PyTorch-ES

For i in range(N):    Обучаем N эпох модель-родителя с помощью SGD    For k in range(K):       Создаем потомка добавлением шума       Высчитываем его скор        Сохраняем потомка и его скор    Получаем нового родителя

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

Получение нового родителя

normalized_rewards = (rewards - np.mean(rewards)) / np.std(rewards) for index, param in enumerate(self.weights):    A = np.array([p[index] for p in population])    rewards_pop = torch.from_numpy(np.dot(A.T,normalized_rewards).T).float()    param.data = param.data + LEARNING_RATE/(POPULATION_SIZE * SIGMA) * rewards_pop

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

Результаты эксперимента
Рыжий/Красный — SGD+эволюция
Голубой/Синий — SGD

C нуля Pretrained

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

Validation — здесь можно посмотреть прогресс моделей на валидационном датасете по метрике accuracy. Можно заметить, что где-то на 5к итерации значение для варианта с классическим SGD стало падать, что говорит о том, что модель начала переобучаться на тренировочном датасете, когда как график SGD+эволюция продолжил расти

Final score — показывает рост метрики accuracy на тестовом датасете и здесь происходило примерно тоже самое, что и на валидации.

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

Финальная таблица по accuracy на тесте

С нуля Pretrained
SGD 47.72% 68.56 %
Метод случайных градиентов 47.81% 68.61 %
SGD + эволюционная стратегия от OpenAI 49.82% 69.45 %

Итог:

  1. Не удалось заставить работать с Adam, возможно, у нас просто закончился энтузиазм. Во всех экспериментах с ним эволюционные методы чуть чуть проигрывали.
  2. Можно было сделать мутации частью оптимизатора, а не писать отдельную оболочку для этого
  3. Ушло в несколько раз больше времени, чем мы планировали

Будем рады обратной связи не только по контенту, но и по самой статье в целом. Если занимаетесь этой темой или интересовались, то тоже пишите, было бы здорово пообщаться, может мы что-то упустили.

Полезные ссылки

  1. Adding Gradient Noise Improves Learning for Very Deep Networks
  2. Evolutionary Stochastic Gradient Descent for Optimization of Deep Neural Networks
  3. Evolution Strategies as a Scalable Alternative to Reinforcement Learning

ссылка на оригинал статьи https://habr.com/ru/post/494686/

Некоторые тенденции в сфере хранения данных, на которые стоит обратить внимание

Мы знаем, что данные становятся все более ценным для бизнеса ресурсом. Известно и то, что объем генерируемых в мире данных будет расти в геометрической прогрессии. Но тогда возникает вопрос: где же компании собираются их хранить? С появлением новых технологий в этой отрасли стали происходить заметные изменения, которые окажут влияние на рынок корпоративных СХД.

Ожидается, что в скором времени резко вырастут объемы видеоматериалов и тех данных, которые собираются с различных датчиков. Последние тенденции приведут к изменениям в самом подходе к хранению информации и управлению ею. ИТ-администраторы и директора должны заранее планировать будущие изменения и быть к ним готовыми.

Стандартизация управления данными

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

Например, компании должны по возможности централизовать администрирование существующих СХД, в идеале — управлять ими через единый интерфейс. Данные будет намного легче сортировать, контролировать и использовать, если их можно будет универсальным образом интерпретировать.

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


Рост мирового рынка СХД с функциями ИИ (AI-Powered Storage). По данным Market Research Future

Решения на основе искусственного интеллекта и машинного обучения для управления и анализа больших объемов данных получат широкое распространение. Особенно с учетом того, что сложность и разнообразие данных продолжают расти.

Гибридные системы хранения и многоуровневое хранение данных (тиринг)

Многие компании предпочитают использовать комбинацию локального хранилища и облачных платформ. Там, где требуется быстрый доступ к большим объемам данных, по-прежнему необходимы сети хранения SAN или другие локальные системы. Однако для резервного копирования и архивирования больше подходит облако. Чтобы оптимизировать распределение ресурсов, используются механизмы многоуровневого хранения — тиринга, которые автоматически определяют оптимальное место для размещения данных.


Используемые технологии хранения данных — сейчас и через два года. По прогнозу Spiceworks Research

Искусственный интеллект и «быстрое» хранение данных

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


Рост продаж СХД для нагрузок ИИ в мире. Источник: IDC

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

Локальные ЦОД для ускорения коммуникаций

Облачным провайдерам все чаще приходится обеспечивать максимально быстрое подключение к корпоративной инфраструктуре. По этой причине новые центры обработки данных, такие как ЦОД Microsoft или Amazon, размещаются «ближе к пользователю». Это помогает устранить или, по крайней мере, свести к минимуму проблемы с медленным подключением к облачному серверу.

Это применимо также и к небольшим региональным облачным провайдерам, которые гораздо более децентрализованы, чем инфраструктура ЦОД Azure или AWS. Им требуется хорошее интернет-соединение, но его проще получить с помощью небольших локальных центров обработки данных. Региональные провайдеры такого типа представляют собой разумный компромисс с точки зрения стоимости и производительности. Они могут выступать в качестве точек высокоскоростного подключения к публичным облакам при реализации мультиоблачных решений.


Использование сервисов облачного хранения данных — сейчас и через два года. По прогнозу Spiceworks Research

Мультиоблачное хранение

Многие компании используют несколько публичных облачных сервисов в сочетании со своей локальной инфраструктурой. Это влияет на процессы перемещения, миграции данных и организации доступа приложений к информации. Системы хранения cloud-ready призваны обеспечить переносимость данных между несколькими облачными платформами, а также между локальной площадкой и облаком.

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

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

Высокопроизводительные вычисления — для среднего бизнеса

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

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

Модули SCM

К новым трендам в отрасли хранения данных можно отнести хранилища объектов для улучшенной индексации и выделения метаданных, а также модули SCM (storage-class memory) для более быстрого доступа к информации с использованием интеллектуальных механизмов тиринга. Благодаря накопителям SCM задержки будут сокращаться до 50%. Энергонезависимая память с производительностью, близкой к скорости работы RAM, позволяет значительно ускорить обработку больших наборов данных.

Кроме того, технология флеш-памяти в виде компонентов SSD продолжит вытеснять в корпоративной среде классические жесткие диски.

SSD и NVMe

Компании будут продолжать заменять традиционные HDD на твердотельные накопители, чтобы повысить производительность, упростить управление и снизить энергопотребление систем. Новые поколения флеш-массивов будут предлагать более совершенные средства автоматизации и защиты данных, а также интеграцию с публичными облаками.

В гораздо большем масштабе будет развертываться протокол NVMe. Линейка массивов хранения Lenovo уже включает в себя хранилища с поддержкой NVMe для увеличения производительности дисковых массивов серии DM.


Массив DM7000F All Flash стал первым на рынке продуктом, который позволил реализовать сквозное решение NVMe over Fabric от сервера до хранилища

Накопители SSD PCIe, использующие NVMe-спецификацию протоколов доступа к твердотельным накопителям, подключенным по шине PCI Express, — одна из основных тенденций развития технологий хранения данных.


Прогноз роста совокупной емкости поставляемых в мире накопителей разного типа. Источник: IDC

По прогнозу IDC, к 2021 году флеш-массивы с подключениями по NVMe и NVMe-oF обеспечат примерно половину всех доходов от поставок внешних систем хранения. Аналитики считают, что у NVMe-oF большой потенциал, поскольку эта спецификация обеспечивает чрезвычайно высокую пропускную способность при сверхнизкой задержке и открывает путь к созданию распределенных СХД с низколатентной фабрикой.

С развитием технологий Lenovo намерена обеспечить защиту инвестиций заказчиков, поскольку существующие системы способны поддерживать новые протоколы без замены оборудования. И новейшая платформа Lenovo NVMe — прямое тому доказательство. СХД с накопителями NVMe демонстрируют свои лучшие качества в работе с базами данных, поэтому для доставки приложений с максимально низкими задержками массив DM7100 поддерживает подключаемые диски NVMe. Это позволяет значительно увеличить производительность СХД в IOPS и построить мощное и масштабируемое хранилище.

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

ссылка на оригинал статьи https://habr.com/ru/company/lenovo/blog/494950/

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

Пока писал эту сугубо техническую статью, Хабр успел превратиться в местное отделение ВОЗ и теперь мне даже стыдно ее публиковать… но в душе теплится надежда, что айтишники еще не разбежались и она найдет своего читателя. Или нет?


Меня всегда восхищала стандартная библиотека Си, да и сам Си — при всей своей минималистичности от них так и веет духом тех самых первых красноглазиков хакеров. В черновике первого официального стандарта (ANSI C, он же C89, он же ANS X3.159-1989, он же, позднее, C90 и IEC 9899:1990) определяется 145 функций и макросов, из них около 25 — это вариации (ввиду отсутствия в языке перегрузок), а 26 чисто математических. K&R во второй редакции² приводят 114 функций (плюс математические), считая остальные за экзотику. В черновике³ C11 функций уже 348, но больше сотни — математика, а еще штук 90 это «перегрузки». А теперь посмотрим на Boost, где одних только библиотек — 160. Чур меня…

И среди этой сотни-полутора функций всегда были: обработка сигналов, вариативные функции (которые до интерпретируемого PHP дошли 25 лет спустя, а в Delphi, бурно развивавшемся одно время, их нет до сих пор) и порядка 50 строковых функций вроде printf() (м-м-м… JavaScript), strftime() (…) и scanf() (дешевая альтернатива регуляркам).

А еще всегда были setjmp()/longjmp(), которые позволяют реализовать привычный по другим языкам механизм исключений, не выходя за рамки переносимого Си. Вот о них и поговорим — Quake World, стеки, регистры, ассемблеры и прочая матчасть, а вишенкой будет занятная статистика (спойлер: Visual Studio непостоянна, как мартовский заяц, а throw saneex.c в два раза быстрее всех).

Скрытый текст

¹ По результатам замеров в статье.

² Кстати, книга великолепная. 270 страниц, из которых 80 — это краткий пересказ стандарта. Или в то время еще не умели растекаться мыслью по древу и конвертировать это в гонорар, или авторы были выше этого. K&R — старая школа, чо.

³ Из особо достоверных источников известно, что финальные версии стандартов ANSI и ISO продаются за деньги, а черновики бесплатны. Но это не точно.

⁴ Да, я тоже не люблю «сокращалки» вроде TinyURL, но парсер Хабра считает URL частью текста и ругается на длинный текст до ката, яко Твіттер поганий. Дальше этого не будет, честно-честно. Параноикам могу посоветовать urlex.org.

Оглавление:

Итак, герои нашей программы — setjmp()/longjmp(), определенные в setjmp.h, которые любят вместе сокращать как «SJLJ» (хотя мне это слово не нравится, напоминает одну печально известную аббревиатуру). Они появились в C89 и, в общем-то, уходить не собираются, но про них не все знают (знать не значит использовать — знание полезно, а использование — как повезет).

Справедливости ради надо сказать, что на Хабре уже были статьи, посвященные этой теме, в особенности отличная статья от zzeng. В англоязычной Сети, конечно, тоже имеется, плюс можно найти реализации вроде такой или даже вот такой¹, но, на мой взгляд, у них есть фатальный недостаток результат или не до конца привычен (к примеру, нельзя выбрасывать исключения повторно), или используются механизмы не по стандарту.

¹ CException хочется отметить особо — всего 60 строчек, пишут, что работает быстро, тоже ANSI C, но у него нет finally и текстовых сообщений, что для меня принципиально важно.

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

Для меня лично исключения это инструмент, который позволяет:

  • не думать в каждом конкретном месте, что что-то может пойти не так, если это самое место все равно с этим ничего не может сделать (ресурсы не заблокированы, память не выделена — можно прерываться немедленно, без if (error) return -1;)
  • когда что-то и впрямь пошло не так — сохранить как можно больше информации, от кода ошибки и имени файла до значения важных переменных и других исключений, которые породили эту ситуацию

Но обо всем по порядку. Как это у нас принято, начнем с матчасти.

Как работают setjmp()/longjmp()

Регистры, стек и все-все-все

В двух словах, longjmp() — это нелокальный goto, а setjmp() — пророк его способ задания метки этому goto в run-time. Короче, «goto на стероидах». И, как и любые стероиды, то бишь, goto, они могут нанести непоправимый вред вашему коду — превратить его в такую лапшу, которая для goto просто вне досягаемости. Посему лучше всего их использовать не напрямую, а внутри какой-нибудь обертки, задающей четкую иерархию переходов (как то исключения — вверх по стеку в пределах явно обозначенных блоков «try»).

Помните, я говорил в начале, что от Си и, конкретно, от setjmp.h прямо веет черт^W юниксовщиной? Так вот, вы вызываете setjmp() один раз, а она возвращается сколько угодно раз (но, как минимум, один). Да, в обычном мире смузихлебы вызывают функцию и она возвращается один раз, а в Советской России функция вызывает вас сама, сколько раз ей хочется и когда ей этого хочется. Такие дела.

Эта концепция, кстати, воплотилась не только в setjmp() — fork() в POSIX делает нечто очень похожее. Я помню, когда я впервые знакомился с *nix’овыми API после десятка лет работы исключительно с WinAPI, мне просто сносило крышу — в моих ментальных шаблонах не укладывалось, что функции могут вот так себя вести. Как метко говорят — «а что, так можно было?»… Но мы отвлеклись.

Думаю, все читающие в курсе, что основной элемент рантайма — это стек, на котором лежат параметры и (некоторые) локальные переменные данной функции. Вызываешь новую функцию — стек растет (причем у Intel’а — вниз), выходишь — тает (у Intel’а — да-да, вверх). Вот примерчик:

void sub(int s) {   char buf[256];   sub(2); }  int main(int m) {   sub(1); }

Есть такой занятный компилятор — tcc (Tiny C Compiler) от известного программиста-парохода Ф. Беллара. tcc практически не делает оптимизаций и код после него очень приятно смотреть в дизассемблере. Он генерирует такое тело для sub()нотации Intel, опуская пролог и эпилог):

sub     esp, 100h       ; выделяем место под локальную переменную mov     eax, 2          ; передаем параметр push    eax call    sub_401000      ; вызываем sub() add     esp, 4          ; очищаем стек после возврата (= cdecl)

Вот схемка происходящего со стеком:

Вот эти оранжевые цифры по центру — это указатель на вершину стека (который у Intel… ну, вы поняли). Указатель хранится в регистре ESP (RSP на x86_64). setjmp() сохраняет текущее значение ESP/RSP, плюс другие служебные регистры, в область памяти jmp_buf, которую вы ему передаете. Если происходит вызов longjmp() далее по курсу (из этой же функции или из подфункции) — указатель восстанавливается и получается, что следом автоматически восстанавливается и окружение функции, где был вызван setjmp(), а все вызванные ранее подфункции моментально завершаются (возвращаются). Эдакий откат во времени, «undo» для рантайма (конечно, с большой натяжкой).

В следующем примере setjmp() поместит в jmp значение указателя FEF8h (FDF0h и т.д. — красные стрелки на схеме выше) и функция продолжит выполнение, как обычно:

void sub(int s) {   char buf[256];   jmp_buf jmp;   setjmp(jmp);   sub(2); }

Но, конечно, есть нюанс™:

  • нельзя прыгать между потоками (setjmp() в одном, longjmp() в другом), потому как, очевидно, у каждого потока свой стек
  • если функция, которая вызвала setjmp(), уже вернулась, то «реанимировать» ее не выйдет — программа впадет в undefined behavior (и это не лечится)

  • компилятор использует регистры для хранения переменных — они, видите ли, быстрее работают! — а регистры, внезапно, хранятся отдельно от стека и, хотя setjmp() могла сохранить их состояние на момент вызова, она и longjmp() не знают, что с ними происходило после вызова setjmp()

Затирание переменных или, по-русски, clobbering

Последний момент особенно интересен. Пример:

#include <stdlib.h> #include <stdio.h> #include <setjmp.h>  int main(void) {   int i;   jmp_buf jmp;   i = rand();   if (setjmp(jmp) == 0) {     i = rand();     printf("%d\n", i);     longjmp(jmp, 1);   } else {     printf("%d\n", i);   } }

Вопрос залу: будут ли числа в консоли совпадать?

Правильный ответ: зависит от воли звезд. Так-то!

Посмотрим, что происходит на примере gcc. Если скомпилировать с -O0, то числа будут совпадать, а в дизассемблере мы увидим вот это:

; int main(void) {   push    ebp             ; пролог (создается stack frame)   mov     ebp, esp        ; EBP указывает на стек ниже ESP (если по схеме)   sub     esp, E0h   ...   call    _rand           ; результат возвращается в EAX   mov     [ebp-D4h], eax  ; это i = rand(); где i на стеке (EBP-D4h)   ... ; if (... == 0) {         ; вызов setjmp() и возврат из нее до прыжка   call    _rand   mov     [ebp-D4h], eax  ; снова i = rand(); на стеке ; printf("%d\n", i);   mov     eax, [ebp-D4h]  ; передаем i со стека как параметр   mov     esi, eax   lea     edi, format     ; передаем строку "%d\n"   mov     eax, 0   call    _printf   ... ; } else {                ; вторичный возврат из setjmp() после прыжка   mov     eax, [ebp-D4h]  ; снова передаем i, как в ветке выше   mov     esi, eax   lea     edi, format     ; "%d\n"   mov     eax, 0   call    _printf

Как видно, компилятор не заморачивался и поместил переменную i в стек (по адресу EBP - D4h). Если смотреть на всю ту же схемку, то:

  • вместо буфера на 256 char мы имеем int и jmp_buf, размер которых на моей системе 4 и 200 байт соответственно, плюс 20 байт для чего-то потребовалось компилятору, так что на стеке под локальные переменные выделилось 224 байта (E0h) вместо 100h, как в том примере
  • ESP на момент вызова setjmp() равен FFF8h - E0h = FF18h (вместо FEF8h), это значение и сохраняется в jmp
    • конечно, это значение условно, в реальности оно будет иным
  • и первое присваивание i, и второе меняют значение i в стеке (по адресу FF18h)
  • longjmp() сбрасывает указатель стека обратно в FF18h, но, так как переменная i не выходит за эти границы, она по-прежнему доступна, равно как и другая переменная (jmp), и параметры main() (буде они есть)
    • в этом примере ESP и так не менялся, но longjmp() легко мог бы быть внутри другой функции, вызванной из main()

А вот если включить хотя бы -O1, то картина изменится:

; пролога и stack frame больше нет, используется значение ESP напрямую   sub     esp, E8h   ...   call    _rand   mov     [esp+E8h-DCh], eax  ; i = rand(); в стеке, как и с -O0   ... ; -O1 почему-то решило, что выполнение else более вероятно, чем ; if (setjmp() == 0) (хотя по-моему наоборот), и переставило ; их местами; здесь я вернул прежний порядок для понятности ; if (... == 0) {   call    _rand   mov     esi, eax            ; ВНИМАНИЕ! запись i в регистр ; printf("%d\n", i);   lea     edi, format     ; "%d\n"   mov     eax, 0   call    _printf   ... ; } else {   mov     esi, [esp+E8h-DCh]  ; ВНИМАНИЕ! чтение i со стека   lea     edi, format     ; "%d\n"   mov     eax, 0   call    _printf

Вдобавок, с -O1 gcc при компиляции ругается страшными словами:

test.c:6:11: warning: variable ‘i’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Wclobbered]

Что мы здесь видим? Вначале i помещается в регистр, но в первой ветке (внутри if) gcc, видимо сочтя i не используемой после первого printf(), помещает новое значение сразу в ESI, а не в стек (через ESI оно передается дальше в printf(), см. ABI, стр. 22 — RDI (format), RSI (i), …). Из-за этого:

  • в стеке по адресу ESP + E8h - DCh остается старое значение rand()
  • в ESI оказывается новое значение
  • printf() (первый вызов) принимает (новое) значение из регистра
  • longjmp() сбрасывает указатель стека, но не восстанавливает изменившиеся регистры, которые используются функциями для локальных переменных при включенных оптимизациях
  • второй вызов printf()else) читает значение, как положено, из стека, то бишь старое
    • но даже если бы оно читалось из ESI, то после прыжка в этом регистре был бы мусор (вероятно, из printf() или самого longjmp())

Или, если переписать это обратно на Си:

stack[i] = rand();          // i = rand(); изменение стека (1) if (setjmp(jmp) == 0) {   ESI = rand();             // i = rand(); изменение регистра (2)   printf("%d\n", ESI);      // печать значения (2)   longjmp(jmp, 1);          // прыжок } else {   printf("%d\n", stack[i]); // печать значения (1)   // или могло бы быть так:   printf("%d\n", ESI);      // использование регистра, где уже кто-то                             // "побывал" (первый printf() или longjmp()) }

Скрытый текст

Честно говоря, мне не понятно, почему gcc результат первого rand() не помещает сразу в ESI или в другой регистр (даже при -O3). На SO пишут, что в режиме x86_64 (под который я компилировал пример) сохраняются все регистры, кроме EAX. Зачем промежуточное сохранение в стек? Я предположил, что gcc отследил printf() в else после longjmp(), но если убрать второй rand() и этот printf() — результат не меняется, i так же вначале пишется в стек.

Если кто может пролить свет на сию тайну — прошу в комментарии.

Квалификатор volatile

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

volatile int i;

Единственное изменение при -O1 будет в теле if:

; было:   call    _rand   mov     esi, eax ; стало:   call    _rand   mov     [rsp+E8h-DCh], eax   mov     esi, [rsp+E8h-DCh] ; или можно переписать так:   call    _rand   mov     esi, eax   mov     [rsp+E8h-DCh], eax

Как видим, компилятор продублировал присвоение в стек (сравните):

if (setjmp(jmp) == 0) {   ESI = stack[i] = rand();

Случаи использования IRL

Итак, если соблюдать меры предосторожности — не прыгать между потоками и между завершившимися функциями и не использовать изменившиеся не-volatile переменные после прыжка, то SJLJ позволяет нам беспроблемно перемещаться по стеку вызовов в произвольную точку. И не обязательно быть адептом секты свидетелей исключений — сопротивление бесполезно, ибо SJLJ уже давно заполонили всю планету среди нас:

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

Конкретно в Quake World запускается бесконечный цикл в WinMain(), где каждая новая итерация устанавливает jmp_buf, а несколько функций могут в него прыгать, таким образом реализуя «глубокий continue»:

// WinQuake/host.c jmp_buf         host_abortserver;  void Host_EndGame (char *message, ...) {   ...    if (cls.demonum != -1)     CL_NextDemo ();   else     CL_Disconnect ();    longjmp (host_abortserver, 1); }  void Host_Error (char *error, ...) {   ...    if (cls.state == ca_dedicated)     Sys_Error ("Host_Error: %s\n",string);  // dedicated servers exit    CL_Disconnect ();   cls.demonum = -1;    inerror = false;    longjmp (host_abortserver, 1); }  void _Host_Frame (float time) {   static double           time1 = 0;   static double           time2 = 0;   static double           time3 = 0;   int                     pass1, pass2, pass3;    if (setjmp (host_abortserver) )     return;                 // something bad happened, or the server disconnected    ... }  // QW/client/sys_win.c int WINAPI WinMain (...) {   ...    while (1)   {     ...     newtime = Sys_DoubleTime ();     time = newtime - oldtime;     Host_Frame (time);     oldtime = newtime;   }    /* return success of application */   return TRUE; }

Производительность

Один из доводов, который приводят против использования исключений — их отрицательное влияние на производительность. И действительно, в исходниках setjmp() в glibc видно, что сохраняются почти все регистры общего назначения ЦП. Тем не менее:

  • само собой разумеется, что ни исключения в общем, ни SJLJ/saneex.c в частности и не предполагаются к применению во внутренностях числодробилок
  • современные те-кхе…кхе-нологии (извиняюсь, электрон в горло попал) таковы, что сохранение лишнего десятка-другого регистров — это самая малая из проблем, которые они в себе несут
  • если скорость критична, а исключений хочется — есть механизмы zero-cost exceptions (или, точнее, zero-cost try), которые радикально снижают нагрузку при входе в блок try, оставляя всю грязную работу на момент обработки (выброса) — а так как исключения это не goto и должны использоваться, гм, в исключительных ситуациях, то на производительности такой «перекос» сказывается, э-э, исключительно положительно

«Честные» zero-cost exceptions особенно полезны в том плане, что избавляют от более медленных volatile-переменных, которые иначе размещаются в стеке, а не в регистрах (именно поэтому они и не затираются longjmp()). Тем не менее, их поддержка это уже задача для компилятора и платформы:

  • В Windows есть SEH и VEH, последний подвезли в XP.
  • В gcc было несколько разных вариантов — вначале на основе SJLJ, потом DWARF, коего на сегодняшний день было пять версий (DWARF применяется и в clang). На эту тему см. отменные статьи zzeng: тыц и тыц, и сайт dwarfstd.org.
  • В комментарии к другой статье камрад nuit дал наводку на интересный проект libunwind, но использовать его только ради исключений — это как стрелять из воробьев по пушкам (больно большой).

И, хотя saneex.c не претендует на пальму zero-cost (ее пальма — это переносимость), так ли уж страшен setjmp(), как его малюют? Может, это суеверие? Чтобы не быть голословными — померяем.

Тестовая среда

Я набросал два бенчмарка «на коленке», которые в main() в цикле 100 тысяч раз входят в блок try/catch и делают или не делают throw().

Исходник бенчмарка на C:

#include <stdio.h> #include <time.h> #include "saneex.h"  int main(void) {   for (int i = 0; i < 100000; i++) {     try {       // либо ("выброс" = да):       throw(msgex("A quick fox jumped over a red dog and a nyancat was spawned"));       // либо ("выброс" = нет):       time(NULL);     } catchall {       fprintf(stderr, "%s\n", curex().message);     } endtry   } }

Исходник на С++ (я адаптировал пример с Википедии, вынеся объявление вектора за цикл и заменив cerr << на fprintf()):

#include <iostream> #include <vector> #include <stdexcept> #include <time.h>  int main() {   std::vector<int> vec{ 3, 4, 3, 1 };    for (int i = 0; i < 100000; i++) {     try {       // либо ("выброс" = да):       int i{ vec.at(4) };       // либо ("выброс" = нет):       time(NULL);     }     catch (std::out_of_range & e) {       // << вместо fprintf() вызывает замедление цикла на 25-50%       //std::cerr << "Accessing a non-existent element: " << e.what() << '\n';       fprintf(stderr, "%s\n", e.what());     }     catch (std::exception & e) {       //std::cerr << "Exception thrown: " << e.what() << '\n';       fprintf(stderr, "%s\n", e.what());     }     catch (...) {       //std::cerr << "Some fatal error\n";       fprintf(stderr, "Some fatal error");     }   }    return 0; }

Тестировалось все на одной машине в двух ОС (обе 64-битные):

  • Windows 10 2019 LTSC под PowerShell с помощью Measure-Command { test.exe 2>$null }
  • последний Live CD Ubuntu с помощью встроенной time

Также я попробовал замерить исключения в Windows через расширения __try/__except, взяв другой пример с Википедии:

#include <windows.h> #include <stdio.h> #include <vector>  int filterExpression(EXCEPTION_POINTERS* ep) {   ep->ContextRecord->Eip += 8;   return EXCEPTION_EXECUTE_HANDLER; }  int main() {   static int zero;   for (int i = 0; i < 100000; i++) {     __try {       zero = 1 / zero;       __asm {         nop         nop         nop         nop         nop         nop         nop       }       printf("Past the exception.\n");     }     __except (filterExpression(GetExceptionInformation())) {       printf("Handler called.\n");     }   } }

Однако вектор включить в цикл не вышло — компилятор сообщил, что:

error C2712: Cannot use __try in functions that require object unwinding

Так как накладываемые ограничения на код идут вразрез с принципом привычности, о котором я говорил в начале, я не внес эти результаты в таблицу ниже. Ориентировочно это 1100-1300 мс (Debug или Release, x86) — быстрее, чем стандартные исключения в VS, но все равно медленнее, чем они же в g++.

Результаты

№   Компилятор        Конфиг    Платф Механизм  Выброс  Время (мс)¹           saneex медленнее  1.  VS 2019 v16.0.0   Debug     x64   saneex.c  да      9713  / 8728  = 1.1   в 1.8 / 1.8 2.  VS 2019 v16.0.0   Debug     x64   saneex.c  нет     95    / 46    = 2     в 4.5 / 2.3 3.  VS 2019 v16.0.0   Debug     x64   C++       да      5449  / 4750² = 1.6 4.  VS 2019 v16.0.0   Debug     x64   C++       нет     21    / 20    = 1 5.  VS 2019 v16.0.0   Release   x64   saneex.c  да      8542³ / 182   = 47    в 1.8 / 0.4 6.  VS 2019 v16.0.0   Release   x64   saneex.c  нет     80³   / 23    = 3.5   в 8   / 1.8 7.  VS 2019 v16.0.0   Release   x64   C++       да      4669³ / 420   = 11 8.  VS 2019 v16.0.0   Release   x64   C++       нет     10³   / 13    = 0.8 9.  gcc 9.2.1         -O0       x64   saneex.c  да      71    / 351   = 0.2   в 0.2 / 0.6 10. gcc 9.2.1         -O0       x64   saneex.c  нет     6     / 39    = 0.2   в 1.5 / 1.1 11. g++ 9.2.1         -O0       x64   C++       да      378   / 630   = 0.6 12. g++ 9.2.1         -O0       x64   C++       нет     4     / 37    = 0.1 13. gcc 9.2.1         -O3       x64   saneex.c  да      66    / 360   = 0.2   в 0.2 / 0.6 14. gcc 9.2.1         -O3       x64   saneex.c  нет     5     / 23    = 0.2   в 1   / 0.6 15. g++ 9.2.1         -O3       x64   C++       да      356   / 605   = 0.6 16. g++ 9.2.1         -O3       x64   C++       нет     5     / 38    = 0.1

Скрытый текст

¹ В столбце Время добавлены замеры одного из читателей на Windows 7 SP1 x64 с VS 2017 v15.9.17 и gcc под cygwin.

² Крайне странный факт: если fprintf() заменить на cerr <<, то время выполнения сократится в 3 раза: 1386/1527 мс.

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

Результаты получились… интересные:

  • Показатели сильно плавают на разных машинах и/или окружениях и особенно «чудит» VS. Чем это вызвано — непонятно.
  • Использование cerr << вместо fprintf() в паре с выбросом исключения в VS в отладочной сборке ускоряет цикл в 3-4 раза (строка 3). ЧЯДНТ?
  • Во всех случаях расходы на блок try в отсутствие throw — мизерные (4-28 мс на 100 тысяч итераций).
  • Не считая «разогнанного» Debug в VS, выброс исключений в saneex.c быстрее, чем во встроенных языковых конструкциях (в 2.3 раза быстрее VS, в 5 раз быстрее gcc/g++), а try без throw — помедленнее, но речь идет о единицах миллисекунд. Вот это поворот!

Что тут можно сказать… Есть о чем похоливарить. Добро пожаловать в комментарии!

Для меня самый важный use-case — это много блоков try с крайне редкими throw («лови много, бросай мало»), а он зависит практически только от скорости setjmp(), причем производительность последнего, судя по таблице, далеко не так плоха, как часто думают. Косвенно это подтверждается и вот этой статьей, где автор после замеров делает вывод, что один вызов setjmp() равен двум вызовам пустых функций в OpenBSD и полутора (1.45) — в Solaris. Причем эта статья от 2005 года. Единственное «но» — сохранять нужно без сигнальной маски, но она обычно и не интересна.

Ну, а напоследок…

Виновник торжества — saneex.c

Библиотека, чей пример был на КДПВ:

  • может компилироваться даже в Visual Studio
  • поддерживает любую вложенность блоков, throw() из любого места, finally и несколько catch на блок (по коду исключения)
  • не выделяет память и не использует указатели (все в static)
  • опционально-многопоточная (__thread/_Thread_local)
  • в public domain (CC0)

Интересующиеся могут найти ее исходники на GitHub. Ниже я кратко на одном примере покажу, как ей пользоваться и какие есть подводные камни. Код примера из saneex-demo.c в репозитории:

01.    #include <stdio.h> 02.    #include "saneex.h" 03. 04.    int main(void) { 05.      sxTag = "SaneC's Exceptions Demo"; 06. 07.      try { 08.        printf("Enter a message to fail with: [] [1] [2] [!] "); 09. 10.        char msg[50]; 11.        thrif(!fgets(msg, sizeof(msg), stdin), "fgets() error"); 12. 13.        int i = strlen(msg) - 1; 14.        while (i >= 0 && msg[i] <= ' ') { msg[i--] = 0; } 15. 16.        if (msg[0]) { 17.          errno = atoi(msg); 18.          struct SxTraceEntry e = newex(); 19.          e = sxprintf(e, "Your message: %s", msg); 20.          e.uncatchable = msg[0] == '!'; 21.          throw(e); 22.        } 23. 24.        puts("End of try body"); 25. 26.      } catch (1) { 27.        puts("Caught in catch (1)"); 28.        sxPrintTrace(); 29. 30.      } catch (2) { 31.        puts("Caught in catch (2)"); 32.        errno = 123; 33.        rethrow(msgex("calling rethrow() with code 123")); 34. 35.      } catchall { 36.        printf("Caught in catchall, message is: %s\n", curex().message); 37. 38.      } finally { 39.        puts("Now in finally"); 40. 41.      } endtry 42. 43.      puts("End of main()"); 44.    }

Программа выше читает сообщение, бросает исключение и обрабатывает его в зависимости от пользовательского ввода:

  • если ничего не ввести — исключение выброшено не будет, и мы увидим:

End of try body Now in finally End of main()

  • если ввести текст, начинающийся с единицы, то будет создано исключение с этим кодом (1), оно будет поймано в первом блоке catch (1) (26.), а на экране появится:

Caught in catch (1) Your message: 1 hello, habr!     ...at saneex-demo.c:18, code 1 Now in finally End of main()

  • если ввести двойку, то исключение будет поймано (30.), выброшено новое (со своим кодом, текстом и прочим) с сохранением предыдущей информации в цепочке (33.), дойдет до внешнего обработчика и программа завершится:

Caught in catch (2) Now in finally  Uncaught exception (code 123) - terminating. Tag: SaneC's Exceptions Demo Your message: 2 TM! kak tam blok4ain?     ...at saneex-demo.c:18, code 2 calling rethrow() with code 123     ...at saneex-demo.c:33, code 123 rethrown by ENDTRY     ...at saneex-demo.c:41, code 123

  • если ввести !, то исключение получится «неуловимым» (uncatchable; 20.) — оно пройдет сквозь все блоки try выше по стеку, вызывая их обработчики (как catch, так и finally), пока не дойдет до внешнего и не завершит процесс — гуманный аналог abort():

Caught in catch (1) Your message: ! it is a good day to die     ...UNCATCHABLE at saneex-demo.c:18, code 0 Now in finally  Uncaught exception (code 0) - terminating. Tag: SaneC's Exceptions Demo Your message: ! it is a good day to die     ...UNCATCHABLE at saneex-demo.c:18, code 0 UNCATCHABLE rethrown by ENDTRY     ...at saneex-demo.c:41, code 0

  • наконец, если ввести тройку, то исключение попадет в catchall (35.), где просто будет выведено его сообщение:

Caught in catchall, message is: Your message: 3 we need more gold Now in finally End of main()

Остальные «фичи»

Потокобезопасность. По умолчанию ее нет, но если у вас нормальный компилятор (не MSVC¹), то C11 спасет отца народов за счет помещения важных переменных в локальную область потока (TLS):

#define SX_THREAD_LOCAL _Thread_local

¹ Последние годы у Microsoft имеются какие-то подвижки на почве open source, но всем по дело идет медленно, хотя и лучше, чем 8 лет назад, так что мы пока держимся.

sxTag (05.) — строка, которая выводится вместе с непойманным исключением в stderr. По умолчанию — дата и время компиляции (__DATE__ __TIME__).

Создание SxTraceEntry (записи в stack trace). Есть несколько полезных макросов — оберток над (struct SxTraceEntry) {...}:

  • newex() — этот был в примере; присваивает __FILE__, __LINE__ и код ошибки = errno (что удобно после проверки результата вызова системной функции, как в примере после fgets(); 11.)
    • код меньше 1 становится 1 (ибо setjmp() возвращает 0 только при первом вызове), поэтому catch (0) никогда не сработает
  • msgex(m) — как newex(), но также устанавливает текст ошибки (константное выражение)
  • exex(m, e) — как msgex(), но также прицепляет к исключению произвольный указатель; его память будет освобождена через free() автоматически:

try {   TimeoutException *e = malloc(sizeof(*e));   e->elapsed = timeElapsed;   e->limit = MAX_TIMEOUT;   errno = 146;   throw(exex("Connection timed out", e)); } catch (146) {   printf("%s after %d\n", curex().message,     // читаем через void *SxTraceEntry.extra:     ((TimeoutException *) curex().extra)->elapsed); } endtry

И, конечно, есть мои любимые designated initializers из все того же C99 (работают в Visual Studio 2013+):

throw( (struct SxTraceEntry) {.message = "kaboom!"} );

Выброс исключения:

  • throw(e) — бросает готовый SxTraceEntry
  • rethrow(e) — аналогично throw(), но не очищает текущий stack trace; может использоваться только внутри catch/catchall
  • thrif(x, m) — макрос; при if (x) создает SxTraceEntry с текстом x + m и «выбрасывает» его
  • thri(x) — как thrif(), только с пустым m

Макросы нужны для удобного «преобразования» результата типичного библиотечного вызова в исключение — как в примере с fgets() (11.), если функция не смогла прочитать ничего. Конкретно с fgets() это не обязательно обозначает ошибку (это может быть просто EOF: ./a.out </dev/null), но других подходящих функций в том примере не используется. Вот более жизненный:

thri(read(0xBaaD, buf, nbyte)); // errno = 9, "Bad file descriptor" // Assertion error: read(0xBaaD, buf, nbyte);

…И «особенности реализации»

Их всего две с половиной (но зато какие!):

  • блок обязан заканчиваться на endtry — здесь происходит завершение процесса при отсутствии обработчика (блока try) выше по стеку
    • эту ошибку компилятор, скорее всего, поймает, ибо try открывает три {, а endtry их закрывает
  • нельзя делать return между try и endtry — это самый жирный минус, но моя фантазия не нашла способов отловить эту ситуацию; принимаются идеи и PR
    • естественно, goto внутрь и наружу тоже под запретом, но разве его кто-то использует? </sarcasm>

Что касается «половины», то это уже разобранный ранее volatile. «Прием» исключения — это повторный вход в середину функции (см. longjmp()), поэтому, если значение переменной было изменено внутри тела try, то такая переменная не должна использоваться в catch/catchall/finally и после endtry, если она не объявлена как volatile. Компилятор заботливо предупредит о такой проблеме. Вот наглядный пример:

int foo = 1; try {   foo = 2;   // здесь можно использовать foo } catchall {   // а здесь уже нет! } finally {   // и здесь тоже! } endtry // и здесь нельзя!

С volatile переменную можно использовать где угодно:

volatile int foo = 1; try {   ...

Итог: как это работает

У каждого потока есть два статически-выделенных (глобальных) массива:

  • struct SxTryContext — информация о блоках try, внутри которых мы сейчас находимся — в частности, jmp_buf на каждый из них; например, здесь их два:

try {   try {     // мы здесь   } endtry } endtry

  • struct SxTraceEntry — текущий stack trace, то есть объекты, переданные кодом снаружи для идентификации исключений; их может быть больше или меньше, чем блоков try:

try {         // один SxTryContext   try {       // два SxTryContext               // ноль SxTraceEntry     throw(msgex("Первый пошел!"));               // один SxTraceEntry   } catchall {               // один SxTraceEntry     rethrow(msgex("Второй к бою готов!"));               // два SxTraceEntry (*)   } endtry } endtry

Если в коде выше вместо rethrow() использовать throw(), то объектов SxTraceEntry (*) будет не два, а один — предыдущей будет удален (stack trace будет очищен). Кроме того, можно вручную добавить элемент в цепочку через sxAddTraceEntry(e).

try и другие элементы конструкции суть макросы (— ваш К. О.). Скобки { } после них не обязательны. В итоге, все это сводится к следующему псевдокоду:

try {                             int _sxLastJumpCode = setjmp(add_context()¹);                                   bool handled = false;                                   if (_sxLastJumpCode == 0) {   throw(msgex("Mama mia!"));        clearTrace();                                     sxAddTraceEntry(msgex(...));                                     if (count_contexts() == 0) {                                       fprintf(stderr, "Shurik, vsё propalo!");                                       sxPrintTrace();                                       exit(curex().code);                                     } else [                                       longjmp(top_context());                                     } } catch (9000) {                  } else if (_sxLastJumpCode == 9000) {                                     handled = true; } catchall {                      } else {                                     handled = true; } finally {                       }                                   // здесь действия в finally { } } endtry                          remove_context();                                   if (!handled) {                                     // как выше с throw()                                   }

¹ Имена с _ в библиотеке не используются, это абстракции.

Думаю, после подробных объяснений, как работает SJLJ, что-то еще здесь комментировать излишне, а потому позвольте откланяться и предоставить слово уже вам.

ссылка на оригинал статьи https://habr.com/ru/post/491084/

6 ошибок в английских словах, которые жутко бесят нейтивов

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

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


В чем проблема? Почему некоторые ошибки настолько распространены?

На самом деле, причин много. Но одна из основных — это омофоны.

Омофоны — это глобальная проблема для изучающих английский как иностранный язык.

Омофоны — слова, которые звучат одинаково или имеют минимальные различия в звучании, но пишутся по-разному и имеют разные значения.

Род — рот
Несу разные вещи — несуразные вещи
Кампания — компания
Плод — плот

В русском языке омофонами также можно считать разные формы глаголов с суффиксами «тся» и «ться».

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

К примеру, в русском языке звук [а] один. В английском их 6: короткий [ʌ] (duck), длинный [a:] (garden), широкий [æ] (family), дифтонг [ai] (ice), дифтонг [aʊ] (cloud), трифтонги [aiə] и [aʊə] (fire, flower).

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

Так сковорода (pan [pæn]) из-за неосторожности превращается в ручку (pen [pen]).

При этом умение правильно выговаривать звуки вообще не соотносится с общим уровнем языка. Даже студент с уровнем fluent может артикулировать звуки как попало. Это совершенно отдельный навык, который нужно тренировать отдельно.

Their, They’re и There

Три этих слова входят в словарь базовой лексики — их изучают уже на уровне Elementary. Сложность в том, что они произносятся абсолютно одинаково — [ðer]. Никаких «зеир» или «зейр».

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

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

Давайте сразу на примере. Каждое из этих трех слов может стоять в начале предложения. Но форма и контекст позволяют безошибочно определить, какое именно слово используется.

  • Оборот «There is / There are» указывает на наличие кого-нибудь или чего-нибудь в определенном месте. Также может использоваться в переносном смысле в значении «Существует». И глаголы «is / are» помогают это понять.
    — There are many reasons to buy PS4. — Есть много причин, чтобы купить PS4.
  • «They’re» — это сокращенная форма от «they are». Начало самого обычного предложения. Следующим словом после нее по логике вещей должно быть существительное или прилагательное, которое должно характеризовать «их».
    — They’re rich people. — Они богатые люди.
  • «Their» — это определитель или детерминанта, которая должна обязательно предшествовать существительному. Но после существительного идет глагол.
    — Their dog is barking. — Их собака лает.

Абсолютно та же ситуация со словами «Your» и «You’re». Поэтому мы не будем выносить их как отдельный пример.

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

К примеру, вы сами помните все правила, по которым склоняются существительные? Если вы не филолог, то мы уверены, что нет. Вы просто берете и склоняете существительное. Так же делают и нейтивы. И из-за этого случаются ошибки.

Weird

Weird — странный, таинственный, причудливый

Очень многие делают здесь ошибку в написании этого слова, потому что их сбивает произношение. Часто пишут не «weird», а «wierd».

Weird — амер. [wɪrd], брит. [wɪəd]

Четко слышно, что сначала идет звук [ɪ], а после него — [ə] (или [r] — они довольно похожи). Поэтому на автомате хочется так же и написать.

В староанглийском существовало слово «wyrd», которое означало «судьба, фатум». Иногда его писали именно как «wierd», но подобное не прижилось.

Шекспир в своей пьесе Макбет использовал другой вариант написания: «Weyward Sisters». Но в изданиях XIX века и современных фраза была заменена на привычное «weird Sisters».

The weird sisters, hand in hand,
Posters of the sea and land,
Thus do go about, about:
Thrice to thine and thrice to mine
And thrice again, to make up nine.
Peace! the charm’s wound up.

пер. М. Лозинский

Сестры вещие везде,
На земле и на воде,
Кругом, кругом водят пляс.
Трижды — этой, трижды — той,
Трижды снова, девять! Стой!
Волшебство заведено.

В общем, если не хочется глубоко лезть в лингвистику и прослеживать этимологию слова «weird» аж до протогерманского, то лучше сразу запомнить, как оно пишется.

Lose и Loose

Lose — пропасть, терять, лишаться.
Loose — неплотно прикрепленный, не затянутый, свободный.

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

Lose — [luːz]
Loose — [luːs]

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

Звуки [s] и [z] — парные. Это значит, что артикуляция их полностью совпадает. Единственное различие в том, что [s] — глухой, то есть, полностью состоит из шума, а [z] — звонкий, состоит из шума и голоса.

Слово «Lose» входит в 1000 базовых слов, поэтому его знают абсолютно все, что учит английский язык как второй. А вот «Loose» не входит, поэтому произношение этих двух схожих слов не сравнивают. А зря.

Отсюда и вытекает крайне популярная ошибка — произносить «lose» со звуком [s]. Причина понятна — никто не проверяет транскрипцию каждого изученного слова. А здесь есть буква «s», поэтому студент и предполагает, что и звук здесь тоже будет [s].

Но для нейтива — это как серпом по причинному месту. Потому что вместо «проигрывать» он каждый раз слышит «неплотный».

It’s и Its

Апостроф в английском языке — это совершенно отдельная тема, потому что фактических ошибок там может быть много. Но самая бесячая — это «it’s» «its».

Естественно, это письменная ошибка, потому что произношение этих слов полностью идентичное.

  • It’s — это сокращение от «it is» или от «it has». То есть, местоимение среднего рода 3-го лица единственного числа и глагол.
  • Its — это притяжательная форма местоимения «it». То есть, она указывает на принадлежность чего-то чему-то.

На самом деле ошибка крайне глупая, потому что использование «it’s» и «its» проходят на уровне elementary. Но при быстрой печати на клавиатуре или написании текста вручную она случается не так уж и редко.

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

Definitely

Definitely — определенно

Нейтивы не устают повторять, что в слове «definitely» нет буквы «а». Потому что очень многие студенты делают в этом слове ошибку.

И не только студенты, но и не слишком грамотные нейтивы. Причина в том, что слово произносится как [ˈdefɪnətli]. Буква «i» передает звук [ə] довольно нечасто, поэтому многие, кто не знает, как правильно пишется слово, предполагают, что там буква «a».

Слово «definately» даже включили в современные онлайн-словари с объяснением, что это неправильное написание слова «definitely».

По результатам небольшого исследования издания Daily Record, слово «definitely» получило первое место в рейтинге слов, при написании которых чаще всего ошибаются.

Штука в том, что даже с происхождением слова можно запутаться. Definitely происходит от латинского «definitus» (определенный, ограниченный), которое в свою очередь происходит от «finis» (граница, конец). Кстати, слова «definitely» и «finish» — родственники.

Многие же ошибочно предполагают, что проверочным словом для «definitely» является «final». Отсюда и вырастает лишняя буква «а».

Нейтивы граммар-наци часто говорят, что человек, который пихает букву «а» в слово «definitely», сам является «A-holl».

Then и Than

Единственный способ научиться правильно употреблять «then» и «than» — зазубрить их. По статистике онлайн-школы английского языка EnglishDom, это одна из наиболее распространенных ошибок у русскоговорящих студентов. Больше половины из всех изучающих английский на уровне Intermediate и ниже путаются в использовании «then» и «than». Давайте разъясним это раз и навсегда.

Than — используется для сравнения
Then — используется для отображения времени

— First I stole panda bear, then we drunk beer together. — Сначала я украл панду, а потом мы вместе выпили пива.
— I’m much better in holding my beer than a panda bear. — Я намного лучше держу пиво, чем панда.

Но есть одна мнемонистическая хитрость, которая поможет раз и навсегда запомнить, как использовать «than» и «then» правильно.

Than — comparison. «Than» используется при сравнении, и в обоих этих словах есть буква «а».
Then — time. «Then» используется для времени, и в обоих этих словах есть буква «е».

На самом деле всех этих ошибок можно избежать, если уделить подобным нюансам чуть больше времени. Учите английский с удовольствием и не ошибайтесь в мелочах!

Онлайн-школа EnglishDom.com — вдохновляем выучить английский через технологии и человеческую заботу

Только для читателей Хабра первый урок с преподавателем по Skype бесплатно! А при покупке занятий получите до 3 уроков в подарок!

Получи целый месяц премиум-подписки на приложение ED Words в подарок.
Введи промокод 6misspells на этой странице или прямо в приложении ED Words. Промокод действителен до 31.03.2021.

Наши продукты:

ссылка на оригинал статьи https://habr.com/ru/company/englishdom/blog/494934/

Как собрать 8-битный домашний компьютер на 5 чипах

Комбинируя старые хаки, типа побочных цветов NTSC, с современным оборудованием, можно собрать удивительно мощную машину

Есть что-то привлекательное в 8-битных компьютерах. Можно собрать самодостаточную систему, которая достаточно мощная для того, чтобы быть дружественной к пользователю, но достаточно простая, чтобы её можно было собрать и запрограммировать самостоятельно. Большая часть 8-битных машин, собранных любителями, работает на классических CPU героической эпохи домашних компьютеров 80-х, в то время, когда миллионы лишних телевизоров были превращены в мониторы. Я и сам собрал такой, на базе Motorola 6809. Я пытался использовать минимальное количество чипов, однако всё равно мне потребовалось 13 вспомогательных ИС для обработки таких вещей, как RAM или последовательный интерфейс. И я задумался: что, если отказаться от классического CPU в пользу чего-то более современного, но всё же 8-битного? Насколько сильно можно сократить количество используемых чипов?

Так появился Amethyst. Как и у классического домашнего компьютера, у него встроенная клавиатура, и он может выдавать аудио и видео. У него есть встроенный язык программирования высокого уровня, чтобы пользователи могли писать свои программы. И он использует всего шесть чипов – один ATMEGA1284P CPU, интерфейс USB и четыре простых ИС.

ATMEGA1284P (или 1284P), появившийся в 2008 году, есть 128 Кб флэш-памяти для хранения программ и 16 Кб RAM. Он работает на частотах до 20 МГц, у него есть встроенные контроллеры на последовательном интерфейсе и 32 цифровых контакта ввода-вывода.

Благодаря встроенной памяти и последовательным интерфейсам я мог отказаться от целого ряда вспомогательных чипов. Я мог выдавать базовые звуки, просто включая I/O контакт с разной частотой, выдавая определённые ноты, хотя и с характерной резкостью квадратной волны. А что насчёт выдачи аналогового видеосигнала? Для этого наверняка же потребуется специальное оборудование?

А потом в конце 2018 года я наткнулся на хак, который в 1970-х использовал Стив Возняк, чтобы обеспечить Apple II вывод цветов. Он известен, как побочные цвета NTSC, и полагался на тот факт, что само цветное телевидение в США было своего рода хаком, родом ещё с 1950-х.

Изначально телевещание в США было чёрно-белым, и использовало относительно простой стандарт NTSC (National Television System Committee). Электронно-лучевые трубки обводили лучом поверхность экрана, ряд за рядом. Амплитуда получаемого видеосигнала задавала яркость луча в каждой конкретной точке ряда. Затем в 1953 году NTSC обновили для поддержки цветного телевидения, так, чтобы система оставалась совместимой с чёрно-белыми телевизорами.

Совместимость достигли путём кодирования информации о цвете в виде синусоиды высокой частоты. Фаза сигнала в определённый момент относительно референсного сигнала («сигнала цифровой синхронизации»), передаваемого перед началом каждого ряда, определяла базовый оттенок цвета. Амплитуда определяла насыщенность. Этот высокочастотный сигнал затем добавлялся к относительно низкочастотному сигналу яркости, создавая т.н. композитное видео, которое по сей день используется на входе множества телевизоров и дешёвых дисплеев для самоделок.


Хитрости телевидения: аналоговый композитный цветной видеосигнал, используемый в телевизорах США (слева вверху) совместим с чёрно-белыми телеприёмниками поскольку высокочастотный синусоидальный цветовой сигнал накладывается на сигнал яркости (пунктиром), определяющий яркость строки развёртки. Фильтрующие элементы отделяют сигналы внутри телевизора. Фаза цветового сигнала по отношению к сигналу цифровой синхронизации определяет оттенок цвета на экране. При достаточно высокой частоте цифровой сигнал (слева внизу) отделяется так, будто это аналоговый сигнал, а различные последовательности битов будут давать различные цвета. В данном примере на один пиксель приходится по два бита, и можно выдавать шесть цветов (видно четыре), но увеличение скорости передачи битов увеличивает возможное количество цветов.

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

В 1970-х инженеры поняли, что эту фильтрующую схему можно с большой пользой применить в потребительских компьютерах, поскольку цифровой сигнал с квадратной волной мог воспроизводить большую часть возможностей композитного сигнала. Поток нулей, отправляемый компьютером на телевизор во время прохода луча ЭЛТ по горизонтали телевизор будет интерпретировать как постоянное низкое аналоговое напряжение, обозначающее чёрный цвет. Поток единиц будет выглядеть как постоянно высокое напряжение, белый цвет. Увеличив битовую частоту можно отправлять более сложные двоичные последовательности, на основе которых фильтрующая схема будет выдавать цветовой сигнал. Такой трюк позволил Apple II показывать до 16 цветов.

Сначала я думал очень быстро переключать I/O контакт, чтобы напрямую генерировать видеосигнал. Но вскоре понял, что мой 1284P, работающий с тактовой частотой 14,318 МГц, не сможет работать достаточно быстро для того, чтобы выводить больше четырёх цветов – у встроенных последовательных интерфейсов уходит по два такта на отправку одного бита, что ограничивает мою частоту величиной 7,159 МГц. Apple II использовал быстрый прямой доступ к памяти, чтобы объединить чип внешней памяти с видеовыходом, пока процессор обрабатывал свои данные, однако у моего компьютера RAM интегрирована в чип, поэтому такой подход бы не сработал.

Я порылся у себя в шкафу и нашёл четыре чипа 7400 – два мультиплексора и два параллельно-последовательных сдвиговых регистра. Я мог настроить восемь контактов 1284P на параллельную работу и одновременно отправлять данные на мультиплексоры и сдвиговые регистры, что превратило бы их в высокоскоростной последовательный поток битов. Таким образом я мог генерировать биты достаточно быстро, чтобы выдавать на экран 215 различных цветов. Однако ценой этого стала большая загрузка процессора – свободными остаются порядка 25% его мощностей.


Amethyst – одноплатный компьютер. Он использует шесть встроенных интерфейсов — CPU, USB, четыре чипа 7400, обеспечивающих графику из 215 цветов. Клавиатурные переключатели припаяны прямо к плате, которая также поддерживает аудио и четыре последовательных I/O порта для периферии, типа игровых контроллеров или устройств хранения. Встроенная виртуальная машина на Forth обеспечивает среду программирования.

Следовательно, мне потребовалось легковесное программное окружение для пользователей, из-за чего я предпочёл стандартному Basic язык Forth. Это старый язык для встроенных систем, и его плюс в том, что он как предоставляет интерактивность, так и эффективно компилирует код. Можно сделать очень многое в очень ограниченных рамках. Поскольку 1284P не позволяет исполнять скомпилированный машинный код напрямую в памяти, код пользователя компилируется в промежуточный байткод. Затем он в виде данных поступает в виртуальную машину, запущенную с флэш-памяти 1284P. Код виртуальной машины написан на ассемблере и настроен для максимально быстрой работы.

Я работаю инженером в Glowforge, и у меня есть доступ к передовым лазерным резакам, поэтому спроектировать и собрать деревянный корпус для компьютера было просто (получился своего рода кивок в сторону деревянной отделки Atari 2600). Механические клавиатурные переключатели припаяны прямо к плате; у клавиатуры есть странность в виде отсутствия длинного пробела, эта клавиша расположена над кнопкой Enter.

Все схемы, файлы для PCB, системный код я выложил на GitHub – вы можете собрать свой Amethyst или улучшить мою систему. Получится ли у вас уменьшить количество используемых чипов?

ссылка на оригинал статьи https://habr.com/ru/post/494822/