Ускорьте runhaskell - PullRequest
       8

Ускорьте runhaskell

15 голосов
/ 17 февраля 2012

У меня есть небольшой тестовый фреймворк. Он выполняет цикл, который выполняет следующее:

  1. Создать небольшой исходный файл на Haskell.

  2. Выполните это с runhaskell. Программа генерирует различные файлы на диске.

  3. Обработка только что созданных файлов на диске.

Это происходит несколько десятков раз. Оказывается, runhaskell занимает подавляющее большинство времени выполнения программы.

С одной стороны, тот факт, что runhaskell удается загрузить файл с диска, разбить его на токены, проанализировать, выполнить анализ зависимостей, загрузить еще 20 КБ текста с диска, токенизировать и проанализировать все это, выполнить полный вывод типа, проверка типов, десагарация в Core, компоновка скомпилированного машинного кода и выполнение вещи в интерпретаторе, все за 2 секунды времени на стене, на самом деле чертовски впечатляет, когда вы думаете об этом. С другой стороны, я все еще хочу сделать это быстрее. ; -)

Компиляция тестера (программы, которая запускает вышеуказанный цикл) привела к небольшой разнице в производительности. Компиляция 20 КБ библиотечного кода, на который ссылаются сценарии, дала довольно заметное улучшение. Но это все еще занимает около 1 секунды на вызов runhaskell.

Сгенерированные файлы Haskell имеют размер чуть более 1 КБ каждый, но фактически изменяется только одна часть файла. Возможно, компиляция файла и использование ключа -e GHC будет быстрее?

В качестве альтернативы, может быть, из-за многократного создания и уничтожения многих процессов ОС это замедляется? Каждый вызов runhaskell, по-видимому, заставляет ОС исследовать путь поиска системы, находить необходимый двоичный файл, загружать его в память (наверняка, он уже находится в кеше диска?), Связывать его с любыми библиотеками DLL и запускать его. Есть ли способ, которым я могу (легко) поддерживать один экземпляр GHC, вместо того, чтобы постоянно создавать и уничтожать процесс ОС?

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

Предложения

Обновление: Переключение на GHC -e (т. Е. Теперь все скомпилировано, кроме одного выполняемого выражения) не оказало заметного различия в производительности. На данный момент кажется довольно ясным, что все это связано с ОС. Мне интересно, могу ли я создать канал от тестера до GHCi и, таким образом, использовать только один процесс ОС ...

Ответы [ 5 ]

9 голосов
/ 17 февраля 2012

Хорошо, у меня есть решение: я создал один процесс GHCi и подключил его stdin к каналу, чтобы я мог отправлять ему выражения для интерактивной оценки.

Несколько довольно крупных рефакторингов программ позже, и весь набор тестов теперь занимает примерно 8 секунд, а не 48 секунд. Это подойдет для меня! : -D

(Для всех, кто пытается это сделать: Ради любви к Богу , не забудьте передать переключатель -v0 на GHCi, иначе вы получите приветственный баннер GHCi! GHCi в интерактивном режиме, даже при -v0 командная строка по-прежнему отображается, но при подключении к каналу командная строка исчезает; я предполагаю, что это полезная функция проектирования, а не случайная авария.


Конечно, половина причины, по которой я иду по этому странному маршруту, заключается в том, что я хочу записать stdout и stderr в файл. Используя RunHaskell, это довольно просто; просто передайте соответствующие параметры при создании дочернего процесса. Но теперь все тестовых случаев выполняются одним процессом ОС, поэтому нет очевидного способа перенаправить stdin и stdout.

.

Решение, которое я придумал, состояло в том, чтобы направить весь тестовый вывод в один файл, и между тестами GHCi распечатывает магическую строку, которая (я надеюсь!) Не появится в тестовом выводе. Затем выйдите из GHCi, взломайте файл и найдите волшебные строки, чтобы я мог разрезать файл на подходящие куски.

3 голосов
/ 24 июня 2012

Вы можете найти полезный код в TBC . У него разные амбиции - в частности, отказаться от тестового шаблона и тестовых проектов, которые могут не скомпилироваться полностью - но его можно расширить с помощью функции watch-directory. Тесты выполняются в GHCi, но используются объекты, успешно созданные кабалой ("runghc Setup build").

Я разработал его для тестирования EDSL со сложным типом хакерских атак, то есть когда тяжелый вычислительный подъем выполняется другими библиотеками.

В настоящее время я обновляю его до последней платформы Haskell и приветствую любые комментарии или исправления.

2 голосов
/ 17 февраля 2012

Если большинство исходных файлов остаются без изменений, вы можете использовать флаг -fobject-code (возможно, вместе с -outputdir) GHC для компиляции некоторых файлов библиотеки.

0 голосов
/ 17 февраля 2012

Если тесты хорошо изолированы друг от друга, вы можете поместить весь тестовый код в одну программу и вызвать runhaskell один раз. Это может не работать, если некоторые тесты создаются на основе результатов других или если некоторые тесты вызывают unsafeCrash.

Полагаю, ваш сгенерированный код выглядит так

module Main where
boilerplate code
main = do_something_for_test_3

Вы можете поместить код всех тестов в один файл. Каждый генератор тестового кода отвечает за запись do_something_for_test_N.

module Main where
boilerplate code

-- Run each test in its own directory
withTestDir d m = do
  cwd <- getCurrentDirectory
  createDirectory d
  setCurrentDirectory d
  m
  setCurrentDirectory cwd

-- ["test1", "test2", ...]
dirNames = map ("test"++) $ map show [1..] 
main = zipWithM withTestDir dirNames tests

-- Put tests here
tests =
  [ do do_something_for_test_1
  , do do_something_for_test_2
  , ...
  ]

Теперь вы получаете только один вызов на runhaskell.

0 голосов
/ 17 февраля 2012

Если вызов runhaskell занимает так много времени, то, возможно, вам следует полностью его устранить?

Если вам действительно нужно работать с изменением кода на Haskell, попробуйте следующее.

  1. Создайте набор модулей с различным содержанием по мере необходимости.
  2. Каждый модуль должен экспортировать свою основную функцию
  3. Дополнительный модуль-обертка должен запускать нужный модуль из набора на основе входных аргументов. Каждый раз, когда вы хотите выполнить один тест, вы будете использовать разные аргументы.
  4. Вся программа компилируется статически

Пример модуля:

module Tester where

import Data.String.Interpolation -- package Interpolation

submodule nameSuffix var1 var2 = [str|
module Sub$nameSuffix$ where

someFunction x = $var1$ * x
anotherFunction v | v == $var2$ = v
                  | otherwise = error ("anotherFunction: argument is not " ++ $:var2$)

|]

modules = [ let suf = (show var1 ++ "_" ++ show var2)  in (suf,submodule suf var1 var2) | var1 <- [1..10], var2 <- [1..10]]

writeModules = mapM_ (\ (file,what) -> writeFile file what) modules
...