Ускорить runhaskell
У меня небольшая тестовая структура. Он выполняет цикл, который выполняет следующие действия:
-
Создайте небольшой исходный файл Haskell.
-
Выполните это с помощью runhaskell
. Программа создает различные файлы на диске.
-
Обработать только что сгенерированные файлы диска.
Это происходит несколько десятков раз. Оказывается, что runhaskell
занимает большую часть времени выполнения программы.
С одной стороны, тот факт, что runhaskell
удается загрузить файл с диска, подделать его, проанализировать, проанализировать зависимость, загрузить еще 20 Кбайт текста с диска, сделать токенизацию и проанализировать все это, выполнить полный вывод типа, проверять типы, desugar на Core, ссылаться на скомпилированный машинный код и выполнять вещь в интерпретаторе, все внутри 2 секунд времени на стене, на самом деле довольно впечатляюще, когда вы думаете об этом. С другой стороны, я все еще хочу ускорить его.; -)
Компиляция тестера (программа, выполняющая вышеуказанный цикл) породила небольшую разницу в производительности. Компиляция 20 Кбайт библиотечного кода, связанного с ссылками на скрипты, вызвала более заметное улучшение. Но он занимает около 1 секунды за вызов runhaskell
.
Сгенерированные файлы Haskell имеют чуть более 1 Кбайт каждый, но только одна часть файла на самом деле изменяется. Возможно, компиляция файла и использование GHC -e
будет быстрее?
В качестве альтернативы, возможно, это накладные расходы на многократное создание и уничтожение многих процессов ОС, которые замедляют это? Кажется, что каждый вызов runhaskell
заставляет ОС исследовать путь поиска системы, найти необходимый двоичный файл, загрузить его в память (конечно, это уже в кэш диска?), Связать его с любыми DLL и запустить его. Есть ли способ (легко) сохранить один экземпляр GHC, вместо того, чтобы постоянно создавать и уничтожать процесс ОС?
В конечном счете, я полагаю, что всегда есть API GHC. Но, насколько я понимаю, это кошмарно сложно использовать, сильно недокументировано и подвержено радикальным изменениям при каждом выпуске GHC. Задача, которую я пытаюсь выполнить, очень проста, поэтому я не хочу делать вещи более сложными, чем необходимо.
Предложения?
Обновление: Переход на GHC -e
(т.е. теперь все скомпилировано, за исключением выполняемого одного выражения) не привело к заметной разнице в производительности. На данный момент кажется довольно ясным, что все ОС накладные. Мне интересно, могу ли я создать трубку от тестера до GHCi и, таким образом, использовать только один процесс ОС...
Ответы
Ответ 1
Хорошо, у меня есть решение: я создал один процесс GHCi и подключил его stdin
к каналу, чтобы я мог отправлять его выражения для интерактивной оценки.
Несколько довольно крупных программных рефакторингов позже, и весь комплект тестов теперь занимает примерно 8 секунд, а не 48 секунд. Это будет для меня!:-D
(Тем, кто еще пытается это сделать: ради любви к Богу, не забудьте передать переключатель -v0
в GHCi, или вы получите приветственный баннер GHCi! При этом, если вы запускаете GHCi в интерактивном режиме, даже с -v0
командная строка все еще появляется, но при подключении к каналу командная строка исчезает, я полагаю, что это полезная конструктивная особенность, а не случайная авария.)
Конечно, половина причины, по которой я иду по этому странному маршруту, - это то, что я хочу записать stdout
и stderr
в файл. Используя RunHaskell
, это довольно легко; просто передайте соответствующие параметры при создании дочернего процесса. Но теперь все тестовые примеры выполняются одним операционным процессом, поэтому нет очевидного способа перенаправления stdin
и stdout
.
Решение, с которым я столкнулся, состояло в том, чтобы направить все тестовые выходные данные на один файл, а между тестами GHCi распечатал магическую строку, которая (я надеюсь!) не появится в тестовом выпуске. Затем закройте GHCi, разберите файл и найдите магические строки, чтобы я мог отрезать файл в подходящие куски.
Ответ 2
Вы можете найти полезный код в TBC. У него разные амбиции - в частности, для тестирования тестовых шаблонов и тестовых проектов, которые не могут быть полностью скомпилированы, но могут быть расширены с помощью функции watch-directory. Тесты выполняются в GHCi, но используются объекты, успешно построенные с помощью cabal ( "runghc Setup build" ).
Я разработал его для тестирования EDSL со сложным хакером типа, т.е. когда тяжелый вычислительный подъем выполняется другими библиотеками.
В настоящее время я обновляю его до последней платформы Haskell и приветствую любые комментарии или исправления.
Ответ 3
Если большинство исходных файлов остаются неизменными, вы можете использовать флаг GHC -fobject-code
(возможно, в сочетании с -outputdir
) для компиляции некоторых файлов библиотеки.
Ответ 4
Если вызов runhaskell
занимает так много времени, то, возможно, вы должны полностью его устранить?
Если вам действительно нужно работать с изменением кода Haskell, вы можете попробовать следующее.
- При необходимости создайте набор модулей с различным содержимым.
- Каждый модуль должен экспортировать основную функцию
- Дополнительный модуль обертки должен выполнить правильный модуль из набора на основе входных аргументов. Каждый раз, когда вы хотите выполнить один тест, вы должны использовать разные аргументы.
- Вся программа скомпилирована статически
Пример модуля:
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
Ответ 5
Если тесты хорошо изолированы друг от друга, вы можете поместить весь тестовый код в одну программу и вызвать 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
.