Как стать автором
Обновить

Узкие места интерпретаторов

Время на прочтение7 мин
Количество просмотров2.1K
Эта заметка рассчитана на молодых программистов, которые уже какое-то время используют или только начинают использовать в работе интерпретируемые языки программирования, но пока еще не изучали принцип работы самого языка.

В наше время, в связи с потенциально не плохими зарплатами и офисного типа работой, программирование стало достаточно популярным среди молодежи. К тому же спросом пользуются достаточно не сложные для первоначального освоения языки программирования: JavaScript, PHP, Perl, Python, Java, C#, Basic,… (как видно все они одного семейства — интерпретаторы). В результате появилось достаточно большое количество работников этой отрасли, которые специально программированию нигде не обучались. Требовался программист на язык “X”, купили книгу “X за 2 недели” и через 3 недели – мы уже пишем какой-то проект на “X”. А спустя несколько тысяч строк кода или после того, как база данных обросла реальными данными, проект начинает нещадно тормозить. Можно, конечно, ”пойти поиграть на барабанах”, пока железо дорастет до вашего проекта, но не всегда и не всех этот вариант устраивает.


В чем же обычно основная проблема? Обычно в отсутствии понимания: что на самом деле происходит при выполнении команды “Y”. Языки программирования как языки общения – одно и то же можно объяснить разными словами. Но в случае с компьютерами, будет лучше, если объяснение будет максимально лаконичным. “Лучше” — в смысле «скорости выполнения». Причем лаконичность должна быть на уровне языка, понятного центральному процессору, а не вам. Я имею в виду, что краткость названия функции, которую вы вызываете в том или ином языке программирования, не влияет на производительность (есть исключения); на производительность влияет то, что творит эта функция в своих недрах. А для этого стоит понимать, как на самом деле компьютер работает с числами, строками, массивами, функциями и так далее.

Чтобы эта заметка не слишком разрасталась, я не буду тут описывать, что и как работает на низком уровне. В некоторых языках это объяснение может отличаться. Подробную информацию можно поискать в интернете, книгах или разобраться самому. При желании могу ответить на возникшие вопросы в комментариях и собрать их все в отдельную заметку. А пока объясню только “откуда ноги растут” у распространенных упущениях и на что стоит обратить внимание.

Для начала давайте разберемся в классах языков программирования. Я бы их разбил на 3 группы (возможно, они так и разбиваются):
  • Ассемблер
  • Компиляторы
  • Интерпретаторы


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

Компиляторы – это более дружественные программисту языки: C, C++, Pascal,… В них гораздо проще что-либо писать за счет того, что такие вещи как условные переходы, циклы, работа с переменными и функциями выведены в синтаксис языка. В результате чего больше не требуется писать множество команд центрального процессора, для реализации сложного цикла. Плюс ко всему, в них реализованы различные конструкции, о которых ЦПУ (центральное процессорное устройство) вообще не в курсе. Но которые существенно облегчают структурировать логику работы программы (классы, объекты, записи, массивы,…). При компилировании, программа переводится на язык, понятный конкретному ЦПУ. Так как Ассемблер и язык, понятный ЦПУ, по сути одно и то же, вы всегда можете перевести откомпилированную программу на Ассемблер (дизассемблирование). Перевести откомпилированную программу на компилируемый язык – значительно более сложная задача, так как некоторые конструкции языка процессора не всегда можно нормально перенести в упрощенный синтаксис компилируемых языков. К тому же все наименования переменных и функций при компиляции теряются, и восстановить их не представляется возможным (за исключением, когда программа откомпилирована в режиме отладки).

Интерпретаторы — эти языки представляют собой высшую ступень эволюции: JavaScript, PHP, Perl, Python, Java, C#, Basic… Их особенность заключается в потенциальной независимости от платформы выполнения приложения.

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

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

Программ же, которые были написаны на интерпретируемых языках, выполняются некой программой-прослойкой, которая в реальном времени читает ваш код и переводит его на язык, понятный выполняемым ЦПУ. В результате вопрос переносимости вашего приложения разработчики интерпретаторов взяли на себя. Теперь им приходится изготавливать эту прослойку для разных систем, чтобы ваша программа работала на них всех. Но так как все системы достаточно отличаются, реализовать абсолютную независимость не всегда удается. Если под Linux есть функция “Z”, а под Windows ее нет, то вам придётся либо обойтись без нее, либо ваша программа будет работать только под Linux (например, функции работы с файловой системой).

Основной недостаток интерпретируемых языков – это скорость их выполнения. Вполне очевидно, что программа, откомпилированная на язык, понятном ЦПУ, при выполнении сразу обрабатывается ЦПУ, в то время как программа, написанная на интерпретируемом языке, сначала должна быть распознана и переведена на язык понятный ЦПУ, и только потом ЦПУ начинает ее выполнять. Современные интерпретаторы обзавелись рядом мер борьбы с этим недостатком. Помимо достаточно качественного оптимизатора и системе кэширования, они переводят вашу программу в байт-код (либо в реальном времени, либо имитируя компиляцию). Теперь программе-прослойке не требуется каждый раз распознавать ваш “рукописный текст”. Она это делает либо только 1 раз, либо вообще не делает (если программа уже была переведена в байт-код). Вместо вашей “рукописи”, она работает с байт-кодом вашей программы. Байт-код очень похож на язык ЦПУ, но это не язык ЦПУ (он более платформо-независимый). Его все еще необходимо переводить на язык ЦПУ. Поэтому очевидно, что слухи о Java, которая работает быстрее C++, заметно преувеличены. И это останется так, пока процессоры не научатся понимать байт-код Java.

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

Функции языка, которые уже откомпилированы



Языки программирования – это не только синтаксис. Это еще и набор готовых библиотек функций для работы с различными данными и устройствами. В компилируемых языках они особо ничем не отличаются, а вот в интерпретаторах есть разница. В некоторых языках, таких как Java, эти функции написаны на языке самого интерпретатора. А в некоторых, таких как JavaScript или PHP, на компилируемых языках, то есть они, на момент выполнения программы, уже откомпилированы и не требуют дополнительно обработки. Таким образом, их вызов не будет требовать каких-то дополнительных обработок, в результате чего их выполнение будет значительно более быстрым, чем если вы напишете тоже самое на этом интерпретируемом языке. Поэтому если у вас в наличии есть возможность выполнить такого рода встроенную функцию, пусть даже она делает что-то лишнее, но решает вашу задачу, попробуйте воспользоваться ей, вместо того, чтобы писать свои сложные или не очень конструкции. Например, для разбиения строки на набор подстрок со сложным условием лучше воспользоваться регулярным выражением, нежели писать свой цикл с ручной обработкой этой же строки.

Сложные по структуре, но легкие в использовании, фреймворки



Помимо набора библиотек функций некоторые энтузиасты пытаются еще и преобразить синтаксис и логику языков, привнося какие-то свои идеи, которые что-то упрощают при работе со структурами и/или данными(jQuery, LINQ, ORM,…). Если язык компилируемый, то это не так страшно. А вот в интерпретаторах слепое погружение в сторонние абстрактные функции пагубно. Да, часто с такими преобразователями действительно удобнее, но это удобство почти всегда достигается за счет скорости работы. Достаточно взглянуть в исходный код этих “помощников” и убедиться, что порой гораздо эффективнее вызвать пару встроенных в язык функций, выполняющих конкретно то, что вам требуется, чем одну универсальную стороннюю, которая внутри выполнит “тонну” кода, прежде чем поймет, что же вы от нее хотите и, наконец, сделает это. Например, в JavaScript’e для получения всех DIV’ов, вы можете напрямую вызвать встроенную функцию «document.getElementsByTagName(“DIV”)», которая сразу вернет вам что надо, либо вызвать красивую функция jQuery «$(“DIV”)», которая выполнит пару регулярных выражений, несколько проверок, “ручное” объединение массивов и только после этого вернет требуемое.

Работа со строками



И, наконец, последнее, на чем я хотел уделить ваше внимание – это работа со строками. В интерпретируемых языках работа со строками стала настолько прозрачной, что тот факт, что это одни из самых ресурсо-затратных операций, абсолютно не очевиден. Этот факт известен, обычно, только тем, кто работал с ними хотя бы в компилируемых языках вручную (там тоже есть функции, облегчающую эту работу). Проблема в том, что при почти любой операции со строками (создание строки, объединение строк, разбиение на подстроки, удаление подстроки, замена подстроки), включается поиск свободного места в памяти, необходимой длинны, под новую строку, и копирование результирующих данных в новое место. Даже такие простые, с первого взгляда, операции как поиск по строке, с приходом таких сложных форматов как UTF-8, не являются особо быстрыми. По сравнению с работой в формате ASCII. Поэтому не следует злоупотреблять строками, где можно обойтись без них. Например, ассоциативные массивы – если есть возможность обойтись нумерованным массивом, обойдитесь!


Стоит отметить, что в функции, которая почти ничего не делает, разницу в производительности между оптимизированным кодом и кодом “на скорую руку”, с современными процессорами, вы можете и не почувствовать. Различие будет более очевидным в местах, где код “на скорую руку” выполняется по много раз (в цикле, в часто вызываемой функции) или где такого кода много.

Успехов!
Теги:
Хабы:
Всего голосов 50: ↑15 и ↓35-20
Комментарии78

Публикации