Ускорить 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.