Через несколько недель назад я писал о сравнении регулярных выражений на основе лексеров в Python и JavaScript. Javascript, работающий на Node.js (V8), оказался намного быстрее, чем Python, и в обоих языках улучшение скорости можно было получить, переключившись на одно регулярное выражение и позволив механизму регулярных выражений выполнять тяжелую работу.
Однако в реальном мире вы обнаружите, что большинство лексеров (особенно лексеров для реальных языков программирования) написаны не так. Они тщательно написаны от руки, чтобы пройти через ввод и отправить соответствующий код обработки токена в зависимости от первого встреченного символа.
Эта техника имеет больше смысла для сложных языков, потому что она гораздо более гибкая, чем регулярные выражения (например, представление вложенных комментариев с регулярными выражениями является большой проблемой). Но мне было любопытно также о его последствиях производительности.
Поэтому я собрал рукописный лексер для того же языка, который использовался в предыдущем тесте, а также применил его к тому же большому файлу, чтобы убедиться, что результаты верны. Его время выполнения, однако, удивило меня. Вот время выполнения лексирования документа большого размера (чем меньше, тем лучше):
Я ожидал, что время выполнения будет намного ближе к версии с одним регулярным выражением; на самом деле я ожидал, что это будет немного медленнее (потому что большая часть работы с движком regex выполняется на более низком уровне). Но это оказалось намного быстрее, более чем в 2,5 раза. Еще один случай, когда реальный мир бьет интуицию.
Мне было лень смотреть, но если реализация регулярных выражений в V8 чем-то похожа на реализацию Python, то чередование многих опций (foo | bar) не так эффективно, как можно было бы ожидать, потому что механизм регулярных выражений не использует реальные DFA, а скорее возвращает обратно. Таким образом, чередование по сути означает итерацию глубоко в движке регулярных выражений С другой стороны, рукописный код кажется чем-то весьма оптимизируемым JIT-компилятором, таким как V8 (типы просты и непротиворечивы), поэтому есть большая вероятность, что он будет преобразован в жесткий машинный код. Добавьте немного встраивания, и скорость не очень маловероятна.
Во всяком случае, вот рукописный лексер. Это значительно больше кода, чем основанного на регулярных выражениях, но я не могу сказать, что его было особенно сложно написать — основная часть заняла чуть больше часа или около того. Если у вас есть какие-либо дополнительные идеи по поводу разницы в скорости, я буду рад услышать о них.
'use strict'; var Lexer = exports.Lexer = function() { this.pos = 0; this.buf = null; this.buflen = 0; // Operator table, mapping operator -> token name this.optable = { '+': 'PLUS', '-': 'MINUS', '*': 'MULTIPLY', '.': 'PERIOD', '\\': 'BACKSLASH', ':': 'COLON', '%': 'PERCENT', '|': 'PIPE', '!': 'EXCLAMATION', '?': 'QUESTION', '#': 'POUND', '&': 'AMPERSAND', ';': 'SEMI', ',': 'COMMA', '(': 'L_PAREN', ')': 'R_PAREN', '<': 'L_ANG', '>': 'R_ANG', '{': 'L_BRACE', '}': 'R_BRACE', '[': 'L_BRACKET', ']': 'R_BRACKET', '=': 'EQUALS' }; } // Initialize the Lexer's buffer. This resets the lexer's internal // state and subsequent tokens will be returned starting with the // beginning of the new buffer. Lexer.prototype.input = function(buf) { this.pos = 0; this.buf = buf; this.buflen = buf.length; } // Get the next token from the current buffer. A token is an object with // the following properties: // - name: name of the pattern that this token matched (taken from rules). // - value: actual string value of the token. // - pos: offset in the current buffer where the token starts. // // If there are no more tokens in the buffer, returns null. In case of // an error throws Error. Lexer.prototype.token = function() { this._skipnontokens(); if (this.pos >= this.buflen) { return null; } // The char at this.pos is part of a real token. Figure out which. var c = this.buf.charAt(this.pos); // '/' is treated specially, because it starts a comment if followed by // another '/'. If not followed by another '/', it's the DIVIDE // operator. if (c === '/') { var next_c = this.buf.charAt(this.pos + 1); if (next_c === '/') { return this._process_comment(); } else { return {name: 'DIVIDE', value: '/', pos: this.pos++}; } } else { // Look it up in the table of operators var op = this.optable[c]; if (op !== undefined) { return {name: op, value: c, pos: this.pos++}; } else { // Not an operator - so it's the beginning of another token. if (Lexer._isalpha(c)) { return this._process_identifier(); } else if (Lexer._isdigit(c)) { return this._process_number(); } else if (c === '"') { return this._process_quote(); } else { throw Error('Token error at ' + this.pos); } } } } Lexer._isnewline = function(c) { return c === '\r' || c === '\n'; } Lexer._isdigit = function(c) { return c >= '0' && c <= '9'; } Lexer._isalpha = function(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_' || c === '$'; } Lexer._isalphanum = function(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_' || c === '$'; } Lexer.prototype._process_number = function() { var endpos = this.pos + 1; while (endpos < this.buflen && Lexer._isdigit(this.buf.charAt(endpos))) { endpos++; } var tok = { name: 'NUMBER', value: this.buf.substring(this.pos, endpos), pos: this.pos }; this.pos = endpos; return tok; } Lexer.prototype._process_comment = function() { var endpos = this.pos + 2; // Skip until the end of the line var c = this.buf.charAt(this.pos + 2); while (endpos < this.buflen && !Lexer._isnewline(this.buf.charAt(endpos))) { endpos++; } var tok = { name: 'COMMENT', value: this.buf.substring(this.pos, endpos), pos: this.pos }; this.pos = endpos + 1; return tok; } Lexer.prototype._process_identifier = function() { var endpos = this.pos + 1; while (endpos < this.buflen && Lexer._isalphanum(this.buf.charAt(endpos))) { endpos++; } var tok = { name: 'IDENTIFIER', value: this.buf.substring(this.pos, endpos), pos: this.pos }; this.pos = endpos; return tok; } Lexer.prototype._process_quote = function() { // this.pos points at the opening quote. Find the ending quote. var end_index = this.buf.indexOf('"', this.pos + 1); if (end_index === -1) { throw Error('Unterminated quote at ' + this.pos); } else { var tok = { name: 'QUOTE', value: this.buf.substring(this.pos, end_index + 1), pos: this.pos }; this.pos = end_index + 1; return tok; } } Lexer.prototype._skipnontokens = function() { while (this.pos < this.buflen) { var c = this.buf.charAt(this.pos); if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { this.pos++; } else { break; } } }
Похожие сообщения: