Когда JavaScript только-только появился на свет, я даже вообразить себе не мог, чтó этот язык позволит делать сегодня. Многие из нас просто не представляют, как широко он используется в современных приложениях Web 2.0, оставаясь совершенно незаметным для пользователя. На сегодняшний день это едва ли не единственный язык, пригодный для браузерных приложений клиентского уровня. Всегда можно, конечно, обратиться к услугам VBScript, но разве лишь для того, чтобы запускать свои приложения в избранных браузерах. Существует и множество других языков, вот только для их поддержки посетителям вашего Web-сайта придется загружать дополнительные компоненты, что, согласитесь, очень неудобно. Когда же нужно написать приложение, которое будет работать с большинством браузеров и при этом создавать пользователям минимальную головную боль, на клиентской стороне вы скорее всего обратитесь к услугам JavaScript (серверная сторона, естественно, предлагает широчайший выбор языков и платформ).

На первых порах JavaScript служил главным образом для того, чтобы причудливо разукрасить Web-страницу анимацией (например, дополнить дождем из падающих картинок) или расцветить ее как новогоднюю елку. Но сегодня ему нашлось и более серьезное применение. Пользователь Facebook, например, отправляет e-mail с помощью единственного DIV-элемента, который позволяет ввести текст сообщения и отправить его, после чего обновляет всю цепочку переписки в окне. И все эти операции производятся без перехода с одной страницы на другую исключительно благодаря JavaScript. Однако этот язык интерпретируемый, а значит — медленный, что создает трудности для Web-разработчиков. К счастью, компьютеры сегодня довольно проворны, и поэтому сайты ведут себя вполне прилично. Но если не заставить JavaScript работать лучше, то вскоре мы можем упереться в стену.

Для решения такой проблемы существует несколько способов. Один из них, хорошо показавший себя с прошлыми языками сценариев, — это динамичная компиляция JIT (Just-in-Time — “с колес”), когда исходный текст программы компилируется в машинный код по мере необходимости. Однако такой подход хорош лишь для тех подпрограмм, которые повторно вызываются снова и снова. Если же к подпрограмме нужно обратиться один-единственный раз (так бывает, когда в ней немного циклов), зачастую гораздо лучше просто интерпретировать ее вместо того, чтобы сначала компилировать, а потом запускать. Ведь процесс компиляции требует заметно больше времени, чем запуск интерпретированной программы. Если же вы запускаете подпрограмму постоянно, скомпилировав ее в первый раз и затем используя уже в скомпилированном виде, вы намного ускоряете процесс.

Чтобы интепретируемые языки работали быстрее, их создатели приняли концепцию JIT и смешали ее с другими. В разрабатываемой сейчас версии Firefox, скажем, технология JIT дополнена компилятором оптимизации JavaScript, что делает ее заметно более быстрой. Хорошая новость, не правда ли? Отметим, кстати, что один из создателей этого языка Брендан Эйх сейчас занимает пост главного инженера Mozilla, где как раз и разрабатывается Firefox. Так что, думаю, этот браузер обязательно сделает крупный вклад в мир JavaScript.

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

Проблемы компиляции JavaScript и других динамичных языков

Тем, кто когда-либо программировал на С++, хорошо известно: прежде чем получить доступ к какому-либо классу, этот класс нужно полностью описать. Обычно описание класса помещается в файл заголовка и включает в себя слово “class”, имя класса и список всех свойств и компонентных функций. И лишь после этого в исходном тексте можно создавать объекты, являющиеся элементами данного класса.

Класс можно сравнить с резаком куки для объектов. Когда программа запущена и вы создаете объект, его свойства сохраняются до тех пор, пока он не будет уничтожен. Значения таких свойств можно менять (свойства высоты в объекте прямоугольника Rectangle, скажем, с 10 до 20 или свойство баланса в объекте CheckingAccount от 100 до 100 000 000), но добавить или удалить сами эти свойства невозможно. Другими словами, пока программа исполняется, добавить в класс Rectangle свойство глубины не удастся. Для этого есть только один путь: внести изменения в исходный текст и перекомпилировать его. Но после этого новое свойство появится во всех объектах Rectangle работающей программы.

Однако JavaScript — язык динамический, поэтому здесь в корне изменена сама концепция классов. Свойства можно добавлять и удалять даже во время исполнения программы. Благодаря этому программист может, например, создать два объекта Rectangle, а затем вставить свойство глубины только в один из них.

По существу здесь появляется и еще одна возможность, правда, довольно сомнительной ценности. Пользователю можно предложить текстовое окно для ввода имени, которое затем будет присвоено вновь созданному под него объекту. Другими словами, пользователь получит право задать для нового объекта имя “eWeek”, а вы добавите такой объект и присвоите ему значение.

Причем в JavaScript это значение может быть любого типа по вашему усмотрению — еще одно существенное отличие от С++. Если в написанной на данном языке программе имеется класс Rectangle (прямоугольник) со свойствами Height (высота) и Width (ширина), которые по вашему определению могут содержать только целые числа, сохранить в них строчную переменную во время исполнения программы не удастся. Заметив такую ошибку, компилятор просто прекратит сборку кода еще до того, как дело дойдет до запуска программы. Но JavaScript — совсем другое дело: для свойства eWeek вполне подойдет и цифра 10, и строка “Ужасное издание”.

А теперь представьте себе, какие серьезные проблемы это создает для компиляторов. С ними уже столкнулась Microsoft, когда попыталась добавить динамические языки в список тех, которые можно компилировать в среде текущего времени .NET. Четкая типизация компонента CLR (Common Language Runtime — общеязыковая исполняющая среда) и его нединамический характер серьезно затрудняют компилирование таких языков, как JavaScript и Python. Корпорация, правда, в конце концов обеспечила поддержку динамических языков (главным образом в семействе Silverlight), но для этого ей пришлось добавить целый уровень таких языков поверх среды исполнения.

А ведь рассматриваться в качестве объекта и иметь поля при прогонке программы может в JavaScript почти всё вплоть до функций.

Чтобы обойти такие препятствия, Google избрала уникальный подход к компиляции JavaScript, реализовав так называемые “скрытые классы”.

Скрытые классы в V8

Google положилась на богатый опыт своей команды по работе с прежними транслируемыми динамическими языками, включая экспериментальные наподобие Self (он был создан, когда члены команды еще работали в Sun Microsystems). Программистам вроде меня подобный подход может показаться просто ужасным. Прочитав о нем впервые, я был, честно говоря, шокирован: как можно идти таким неэффективным путем? Однако все сработало прекрасно.

Вот как работает такая концепция. В JavaScript объекты генерируются сначала без свойств. Когда программист создает два новых объекта, ни один из них свойств не имеет, но затем написанный код может добавить их. Первая строка исходного текста, допустим, создает сам объект, вторая добавляет к нему свойство под названием Width (ширина), а следующая за ней — свойство Height (высота). При исполнении такого кода объект проходит через три стадии: сначала в нем нет свойств, затем имеется только одно свойство, после чего их становится два.

Далее такой код с добавленными свойствами помещается в специально именованную функцию-конструктор, которая запускается при создании каждого нового объекта. Ей присваивается имя, в свою очередь используемое для именования класса объектов. Скажем, у нас имеется функция Rectangle. Сначала мы вставили в нее строку для свойства Width, а затем еще одну — для свойства Height. Вот здесь-то и начинает блистать механизм V8 — движущая сила браузера Google Chrome.

При сборке кода “восьмицилиндровый” движок создает теневой компилированный класс (так же, как при исполнении программы на С++), не содержащий свойств. Затем, дойдя до строки кода, которая добавляет свойство Width, компилятор создает второй класс, на этот раз со свойством Width, а после перехода на следующую строку — еще и третий, со свойствами Width и Height.

То есть появляется сразу три класса. Но компилятор в Google Chrome интеллектуальный, и поэтому когда в коде прописано несколько экземпляров класса Rectangle, он обращается только к третьему из созданных классов.

Это, конечно, пример довольно простой. Если к одному из своих объектов за пределами конструктора вы добавите свойство eWeek, компилятор создаст четвертый класс, куда добавит еще и это свойство.

Другими словами, в Google Chrome допускается присутствие нескольких экземпляров Rectangle, причем одни из них будут сообща использовать третий класс, а другие — относиться к собственным классам. Это и называется скрытыми классами.

Остальной исходный текст, естественно, компилируется в машинный код — компилятор не просто создает массу классов. Именно этим и объясняется высокая эффективность механизма V8. По большому счету идея генерации всех этих классов — компромисс незначительный, так как в конце концов программа исполняется в машинном коде и это происходит во много-много раз быстрее, чем в случае с традиционным интепретируемым языком JavaScript.

Уборка мусора

В языках наподобие JavaScript одна из самых серьезных проблем связана с уборкой “мусора”. Под этим термином понимается очистка использованной памяти: компьютер просто не может снова записать что-то в занятые ячейки, поскольку не знает, нужны ли хранящиеся здесь данные какой-нибудь программе. Все понятно?

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

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

Но проблема даже в другом: объект может быть сохранен как свойство другого объекта. В этом случае исполнительная программа не может удалить первый объект, если используется второй! Ну и кавардак, скажете вы! Что ж, для не слишком-то благозвучного термина “уборка мусора” есть несколько причин, и думаю, кое-кому хотелось бы заменить “мусор” на что-нибудь более приятное, а заодно помочь другим расчистить… гм! — кавардак.

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

В своей документации для разработчиков Google описала изощренный подход к очистке памяти, задействованный в Chrome. Сборщик мусора здесь приостанавливает все другие операции и работает очень быстро, так как убирает только то, что необходимо, а закончив работу, дает “добро” на возобновление замерших процессов. У меня, правда, такой подход вызывает определённое подозрение, но не могу не признать: это все же лучше, чем мучительно пытаться продолжать работу на протяжении долгих минут, пока сборщик мусора делает свое дело. К тому же любые действия пользователя только мешают очистке памяти, и время торможения растягивается еще больше. Так что новый способ следует признать более эффективным.

Заключение

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

Но зайдя на него из Chrome, я был просто поражен! При первом вызове окно Compose, правда, открывалось неторопливо и кнопки форматирования появлялись довольно медленно, но в следующий раз оно возникло моментально, и у меня появилось ощущение, будто я имею дело с приложением для настольной системы, а не интернетовским. Это, конечно, частное наблюдение, не имеющее ничего общего с эталонным тестированием, но факт остается фактом: все здесь идет очень быстро.

Так все-таки быстрее ли работает движок V8 из Chrome, чем новейшие разработки из Firefox? А может, Microsoft втайне готовит что-то еще более скоростное? (Готов спорить, что корпорация разрабатывает собственные компиляторы, но наверняка этого сказать не могу.)

Пока что вердикт по этому вопросу не вынесен. На некоторых страницах я видел “доказательства” со ссылкой на разные эталоны, что V8 работает шустрее нового механизма Firefox. Но попадались мне и сайты, где утверждалось прямо противоположное. Так что с уверенностью можно сказать лишь одно: оба эти браузера работают быстро, буквально летают. И это — отличная новость для разработчиков и еще лучшая для пользователей по всей нашей планете, которые уже устали без конца воевать с тормозными приложениями Web 2.0.