2016-11-16 3 views
1

Я пытаюсь написать простой лексер в clojure. Пока что он распознает только выделенные пробелы.Медленный лексер в clojure

(refer 'clojure.set :only '[union]) 

(defn char-range-set 
    "Generate set containing all characters in the range [from; to]" 
    [from to] 
    (set (map char (range (int from) (inc (int to)))))) 

(def ident-initial (union (char-range-set \A \Z) (char-range-set \a \z) #{\_})) 

(def ident-subseq (union ident-initial (char-range-set \0 \9))) 

(defn update-lex [lex token source] 
    (assoc (update lex :tokens conj token) :source source)) 

(defn scan-identifier [lex] 
    (assert (ident-initial (first (:source lex)))) 
    (loop [[c & cs :as source] (rest (:source lex)) 
     value [(first (:source lex))]] 
    (if (ident-subseq c) 
     (recur cs (conj value c)) 
     (update-lex lex {:type :identifier :value value} source)))) 

(defn scan [{tokens :tokens [c & cs :as source] :source :as lex}] 
    (cond 
    (Character/isWhitespace c) (assoc lex :source cs) 
    (ident-initial c) (scan-identifier lex))) 

(defn tokenize [source] 
    (loop [lex {:tokens [] :source source}] 
    (if (empty? (:source lex)) 
     (:tokens lex) 
     (recur (scan lex))))) 

(defn measure-tokenizer [n] 
    (let [s (clojure.string/join (repeat n "abcde "))] 
    (time (tokenize s)) 
    (* n (count "abcde ")))) 

Lexer обрабатывает около 6 миллионов символов в течение 15 секунд.

=> (measure-tokenizer 1000000) 
"Elapsed time: 15865.909399 msecs" 

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

Кроме того, я реализовал аналогичный алгоритм в C++. Для одного входа требуется всего 0,2 секунды.

Мой вопрос: Как я могу улучшить свой код? Возможно, я неправильно использую структуры данных clojure?

UPDATE:

Так вот мой код C++.

#include <iostream> 
#include <vector> 
#include <chrono> 
#include <unordered_set> 
#include <cstdlib> 
#include <string> 
#include <cctype> 
using namespace std; 

struct Token 
{ 
    enum { IDENTIFIER = 1 }; 
    int type; 
    string value; 
}; 

class Lexer 
{ 
public: 
    Lexer(const std::string& source) 
     : mSource(source) 
     , mIndex(0) 
    { 
     initCharSets(); 
    } 

    std::vector<Token> tokenize() 
    { 
     while (mIndex < mSource.size()) 
     { 
     scan(); 
     } 

     return mResult; 
    } 

private: 

    void initCharSets() 
    { 
     for (char c = 'a'; c <= 'z'; ++c) 
     mIdentifierInitial.insert(c); 
     for (char c = 'A'; c <= 'Z'; ++c) 
     mIdentifierInitial.insert(c); 
     mIdentifierInitial.insert('_'); 

     mIdentifierSubsequent = mIdentifierInitial; 
     for (char c = '0'; c <= '9'; ++c) 
     mIdentifierSubsequent.insert(c); 
    } 

    void scan() 
    { 
     skipSpaces(); 

     if (mIndex < mSource.size()) 
     { 
     if (mIdentifierInitial.find(mSource[mIndex]) != mIdentifierInitial.end()) 
     { 
      scanIdentifier(); 
     } 

     mResult.push_back(mToken); 
     } 
    } 

    void scanIdentifier() 
    { 
     size_t i = mIndex; 

     while ((i < mSource.size()) && (mIdentifierSubsequent.find(mSource[i]) != mIdentifierSubsequent.end())) 
     ++i; 

     mToken.type = Token::IDENTIFIER; 
     mToken.value = mSource.substr(mIndex, i - mIndex); 
     mIndex = i; 
    } 

    void skipSpaces() 
    { 
     while ((mIndex < mSource.size()) && std::isspace(mSource[mIndex])) 
     ++mIndex; 
    } 

    unordered_set<char> mIdentifierInitial; 
    unordered_set<char> mIdentifierSubsequent; 
    string mSource; 
    size_t mIndex; 
    vector<Token> mResult; 
    Token mToken; 
}; 

void measureBigString(int n) 
{ 
    std::string substri = "jobbi "; 
    std::string bigstr; 
    for (int i =0 ;i < n;++i) 
     bigstr += substri; 

    Lexer lexer(bigstr); 

    std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); 

    lexer.tokenize(); 

    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); 

    std::cout << n << endl; 
    std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() <<std::endl; 
    std::cout << "\n\n\n"; 
} 



int main() 
{ 
    measureBigString(1000000); 


    return 0; 
} 
+2

Включите '* warning-on-reflection *', и вы увидите, что вы должны изменить его на: '(Character/isWhitespace^char c)', это дает ускорение x7. – ClojureMostly

+0

Ну, это изменение дает ускорение x3-x5, на самом деле. Но это неплохо. – Hanik

+0

Можете ли вы показать нам код на C++? – ClojureMostly

ответ

2

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

Мое предположение, при котором вещи медленно:

  • проверки символа в наборе (требуется хэширование и пересекающий внутренний хэш-дерево). Вместо создания наборов, создавая функции, которые фактически выполняли проверку на основе символов в диапазонах символов (> это, <, и т. Д.), Не были бы такими же хорошенькими, но почти наверняка были бы быстрее, особенно если вы позаботились об использовании примитивного типа подсказки и избегать бокса на объекты.
  • каждый раз через петлю вводится значение, вложенное в хэш-карту. Это не самая быстрая операция. Если вы сохранили эту вещь как независимый переходный вектор, который будет быстрее и не будет перестраивать верхнее дерево. В зависимости от того, как далеко за пределами идиоматического Clojure и на землю Java вы хотели пойти, вы также можете использовать изменяемый ArrayList. Это грязно, но это быстро - если вы ограничиваете сферу действия того, кто подвергается этому изменяемому состоянию, тогда я бы подумал о чем-то подобном. Концептуально то же, что и переходный вектор.
+0

Спасибо за ваши предложения. Вот мои наблюдения. Изменение наборов для соответствия диапазонов с '<=' не дало результата. Использование ArrayList было таким же, как в случае переходных процессов. Я попытался использовать индексы для строки вместо seq - это значительно замедлило время выполнения. Самый большой выигрыш - использование подсказок типа. – Hanik

2

UPDATE:

Еще один существенный настройка на вектор-де-структурирование. Заменив такой код:

(let [[c & cs] xs] ...) 

с:

(let [c (first xs) 
     cs (rest xs)] ...) 

даст другой x2 улучшение производительности. Все вместе вы получите x26 speedup - который должен быть наравне с реализацией C++.

Так короче:

  1. Тип намек избегает всего вызова отражения
  2. записи дает вам оптимизированный доступ/обновление свойствам
  3. первое и остальное избегает вектора-де-структурирования - который использует энным/nthFrom и выполняет последовательный доступ для seq.

Надеемся, что векторная деструктуризация может быть оптимизирована, чтобы избежать nthFrom для обычного случая, подобного этому (там, где в переплете есть только первые и остальные).


ПЕРВЫЙ TUNING - с типом подсказкой и записью:

Вы также можете использовать запись вместо родовых карт:

(refer 'clojure.set :only '[union]) 

(defn char-range-set 
    "Generate set containing all characters in the range [from; to]" 
    [from to] 
    (set (map char (range (int from) (inc (int to)))))) 

(def ident-initial (union (char-range-set \A \Z) (char-range-set \a \z) #{\_})) 

(def ident-subseq (union ident-initial (char-range-set \0 \9))) 

(defrecord Token [type value]) 
(defrecord Lex [tokens source]) 

(defn update-lex [^Lex lex ^Token token source] 
    (assoc (update lex :tokens conj token) :source source)) 

(defn scan-identifier [^Lex lex] 
    (let [[x & xs] (:source lex)] 
    (loop [[c & cs :as source] xs 
      value    [x]] 
     (if (ident-subseq c) 
     (recur cs (conj value c)) 
     (update-lex lex (Token. :identifier value) source))))) 

(defn scan [^Lex lex] 
    (let [[c & cs] (:source lex) 
     tokens (:tokens lex)] 
    (cond 
     (Character/isWhitespace ^char c) (assoc lex :source cs) 
     (ident-initial c)    (scan-identifier lex)))) 

(defn tokenize [source] 
    (loop [lex (Lex. [] source)] 
    (if (empty? (:source lex)) 
     (:tokens lex) 
     (recur (scan lex))))) 

(use 'criterium.core) 

(defn measure-tokenizer [n] 
    (let [s (clojure.string/join (repeat n "abcde "))] 
    (bench (tokenize s)) 
    (* n (count "abcde ")))) 

(measure-tokenizer 1000) 

Использования критериума:

Evaluation count : 128700 in 60 samples of 2145 calls. 
      Execution time mean : 467.378916 µs 
    Execution time std-deviation : 329.455994 ns 
    Execution time lower quantile : 466.867909 µs (2.5%) 
    Execution time upper quantile : 467.984646 µs (97.5%) 
        Overhead used : 1.502982 ns 

Сопоставляя до исходного кода:

Evaluation count : 9960 in 60 samples of 166 calls. 
      Execution time mean : 6.040209 ms 
    Execution time std-deviation : 6.630519 µs 
    Execution time lower quantile : 6.028470 ms (2.5%) 
    Execution time upper quantile : 6.049443 ms (97.5%) 
        Overhead used : 1.502982 ns 

Оптимизированная версия составляет примерно x13 ускорение. При n = 1,000,000, теперь она занимает ~ 0,5 секунды.

+0

Это замечательно быстро, учитывая, что Clojure помещает каждый символ в текст. – Thumbnail

Смежные вопросы