Реклама в Telegram-каналах DzikPic и dev.by теперь дешевле. Узнать подробности 👨🏻‍💻
Support us

О языках, виртуальных машинах, оптимизации и традиционных взглядах

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

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

Ниже о том, какими технологиями оптимизации мы в настоящее время располагаем

Итак

#1: Типы не обязательно должны быть статическими

Виртуальная машина Java (JVM) и прочие среды времени исполнения с динамической оптимизацией это подтверждают. Во время исполнения можно собрать ту же информацию, которую статические типы способны вам предоставить во время компиляции. Таким образом, в динамических средах можно добиться, как минимум, не худшей оптимизации, чем в полностью статически типизированном и статически оптимизированном коде. В некоторых случаях динамическая среда даже справляется с задачей лучше, поскольку профилирование среды времени исполнения основано на реальном выполнении, реальных процентных соотношениях веток, реальном поведении, а не на догадках о том, что может и чего не может программа. Пожалуй, можно даже утверждать, что статическая оптимизация — это проблема остановки, а динамическая оптимизация по определению превзойдет ее рано или поздно, так как при динамической оптимизации мы улучшаем непосредственно функционал программы.
Правда, вышеизложенное справедливо лишь тогда, когда неукоснительно соблюдается следующее ключевое условие:

#2: Типы должны быть предсказуемыми

Чтобы оптимизация средой времени исполнения происходила как следует, объекты должны иметь предсказуемые типы, а сами типы — обладать предсказуемой структурой. Я не утверждаю, что для типов совершенно необходимы статические объявления. Просто при повторных обращениях типы должны выглядеть одинаково. Если объекты могут изменять тип (become в Smalltalk, слабый контроль типов в Perl и C), то вам приходится закладывать дополнительные контрольные условия для этих изменений. Если этого не сделать, то при любых изменениях приходится браковать значительное количество кода. В случае с языком C все вообще может пойти коту под хвост, если компоненты будут функционировать непредсказуемым образом. Если изменение допустимо и предоставляется на уровне языка, то вы, возможно, просто не сможете справиться со всеми разнообразными формами типов, оптимизация на этом закончится.

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

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

#3: Не пытайтесь обмануть процессор

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

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

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

Языки программирования, их сильные и слабые стороны

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

Dart — динамически типизированный язык (типы как минимум являются опциональными и виртуальная машина может о них не беспокоиться), но его типы имеют неизменную форму. Если программиста не смущают такие типы, то Dart покажется ему очень красивым динамическим языком. В  нем вполне можно добиться такой же оптимизации, как в статически типизированном Java и статически компилируемом C++.

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

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

Разумеется, говоря «это невозможно», я имею в виду «невозможно в обычном случае». Существуют конкретные приложения, созданные для закрытых экосистем. Такие приложения действительно можно значительно оптимизировать, как если бы задействованные типы и объекты имели фиксированную форму. Я немного экспериментировал в этой области с моим компилятором RubyFlux, который статически анализирует входящий код Ruby и предполагает, что встречающиеся ему определенные методы и видимые им поля окажутся единственными вариантами методов и полей, с которыми придется иметь дело. В таком случае в коде приходится не задействовать возможностей языка, способных изменять структуру типа и объекта. В противном случае, вам потребуется каким-то образом узнавать, какие типы или объекты будут затрагиваться этими возможностями. Если вы все это учтете, то вас получится действительно умный компилятор.

Python обладает примерно такими же структурными сложностями, как и Ruby, а также не менее сложным стеком вызовов. В этом случае даже состояние исполнения в стеке не является безопасным. Виртуальная машина даже не может ничего гарантировать в отношении тех значений, с которыми она обращается или о форме активации конкретного вызова. PyPy отлично справляется с этой проблемой, переписывая исполняемый в данный момент код и поднимая стековое состояние в кучу при доступе к нему. Но при таком подходе мы не можем сбрасывать неиспользуемое локальное состояние (так как не можем спрогнозировать, какому компоненту может понадобиться его видеть). Кроме того, метод не работает при параллельном исполнении (мы не можем переписывать код, который в тот же момент может выполняться в другом потоке). Опять же, динамичность «крутой» возможности приводит к неизбежным издержкам, которые можно свести к минимуму, но не исключить.

Короче, Склифосовский!

Итак, к чему я клоню? Как-то вечером я решил разобраться со сравнительной статьей, в которой по одному параметру сравниваются виртуальная машина Java и виртуальная машина Dart. Цифры были не слишком впечатляющими. При построчном портировании с Dart на Java полученный код немного проигрывал в производительности. Стоило немного оптимизировать код Java — и Java незначительно вырывался вперед. После незначительной модификации кода на Dart опять Dart оказывался впереди. Но это неинтересно, поскольку и в Dart, и в Java можно полагаться на то, что формы объектов и типов будут оставаться единообразными, и в результате оптимизаций эти языки примерно сравняются друг с другом. В самых важных аспектах языки достаточно схожи, и виртуальные машины могут не учитывать их разницу.

Где, в таком случае, оказываются такие языки, которые нравятся мне, например, Ruby? Вероятно, следует честно признать, что Ruby в принципе не может достичь такой чистой прямолинейной производительности, как типо-статические (а не статически типизированные) языки вроде Dart и Java. И это независимо от того, какие технологии вы задействуете на виртуальной машине. Приблизиться к этим показателям можно; так, в JRuby при помощи invokedynamic можно сделать вызовы методов практически такими же быстрыми, как в Java. Генерируя формы объектов, можно сделать состояние объектов практически таким же предсказуемым, как у типов Java, но постоянно это делать не получится. Независимо от того, насколько хороша исходная виртуальная машина, вы должны придерживаться ее прописных истин. Противоречить им — все равно что идти против ветра. Ruby на виртуальной машине Dart будет работать, пожалуй не лучше чем Ruby на JVM. В обоих случаях вам придется реализовывать изменяемые типы и способные к росту объекты практически одинаковыми способами. Ruby на Pypy может достичь большего, поскольку эта виртуальная машина ориентирована на работу с изменяемыми типами и растущими объектами, но, возможно, вам потребуется пожертвовать параллелизмом или признать, что чистая производительность при манипуляции объектами не приблизится к уровню, доступному на Java или Dart. Верно и обратное: языки, в которых существуют описанные гарантии статичности типов могут превосходить динамические языки, если выполнять их на динамических виртуальных машинах (например, dart2js). Это происходит по тем же причинам, по которым языки со статическими типами блещут на своих виртуальных машинах: они обеспечивают более непротиворечивую картину мира, и не подкидывают виртуальной машине таких сюрпризов, которые могли бы помешать оптимизации. Вы обмениваете динамичность на уровне языка на предсказуемость на уровне виртуальной машины.

Вывод

Думаю, самое важное – осознать неизбежность конфликта между тем, что программисты пытаются выжать из языка и тем, что язык фактически может им дать. Мы не живем в волшебном мире, где каждый язык может в любых условиях угнаться за любым другим языком, так как невозможно предсказать, каким именно способом будет выполняться конкретная программа (точнее говоря — как она будет выполняться с учетом заданной генеральной стратегии). И это нормально. Большинство языков могут соперничать в производительности друг с другом, а со временем динамические языки со строго оформленными объектами могут предложить способы понемногу избавиться от такой динамичности. Либо этого не произойдет, и языки просто «смирятся» с существующими ограничениями. Но пользователям языков важно сознавать: ничто не дается даром. Нужно понимать подоплеку языковых возможностей и учитывать это при принятии решений при проектировании и реализации программ.

Чарльз Наттер

Источник

Новый рекламный формат в наших телеграм-каналах.

Купить 500 символов за $150

Читайте также
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
Собрали 10 платных и бесплатных онлайн-курсов для изучения SQL. Программы рассчитаны на слушателей, которые только начинают или продолжают знакомство с языком.
10 способов научиться программировать самостоятельно
10 способов научиться программировать самостоятельно
10 способов научиться программировать самостоятельно
Хотите научиться кодить и освоить алгоритмы? Собрали десять советов с чего начать изучение программирования для тех, кто только начинает своё путешествие в мир программирования и снабдили все это полезными ссылками на курсы для начинающих программистов.
7 отличных курсов по финансам. Уплыть «с галеры» и основать свой стартап
7 отличных курсов по финансам. Уплыть «с галеры» и основать свой стартап
7 отличных курсов по финансам. Уплыть «с галеры» и основать свой стартап
Если вы посмотрели «Волк с Уолл-стрит» и хотите, как Леонардо ди Каприо прогуливаться по яхте с бокалом вина в руках, но не знаете, с чего начать, подборка курсов Digitaldefynd станет для вас отличным стартом. Здесь представлены как платные, так и бесплатные программы, которые помогут вам освоить финансовое моделирование. Они подойдут не только для начинающих слушателей, но и для экспертов.
Не Paint-ом единым. 13 курсов по UX/UI-дизайну для продвинутых и не только
Не Paint-ом единым. 13 курсов по UX/UI-дизайну для продвинутых и не только
Не Paint-ом единым. 13 курсов по UX/UI-дизайну для продвинутых и не только
Если вам нравится думать о том, как с минимальными затратами получить максимум эффективности, то проектирование пользовательских интерфейсов определенно вас заинтересует. DigitalDefynd сделал подборку курсов по UX/UI-дизайну как для новичков, так и для продвинутых специалистов. 

Хотите сообщить важную новость? Пишите в Telegram-бот

Главные события и полезные ссылки в нашем Telegram-канале

Обсуждение
Комментируйте без ограничений

Релоцировались? Теперь вы можете комментировать без верификации аккаунта.

Комментариев пока нет.