R:Оптимизация/Компиляция в байт-код

Материал Psylab.info - энциклопедии психодиагностики
Перейти к: навигация, поиск




В данном материале рассматривается один из способов ускорения выполнения кода путём компиляции выражений, функций и скриптов R в байт-код. Байт-код - машинно-независимый код низкого уровня, генерируемый транслятором и исполняемый интерпретатором. Большинство инструкций байт-кода эквивалентны одной или нескольким командам ассемблера. Трансляция в байт-код занимает промежуточное положение между компиляцией в машинный код и интерпретацией.

Программа на байт-коде обычно выполняется интерпретатором байт-кода (обычно он называется виртуальной машиной, поскольку подобен компьютеру). Преимущество - в портируемости, т. е. один и тот же байт-код может исполняться на разных платформах и архитектурах. То же самое преимущество дают интерпретируемые языки. Однако, поскольку байт-код обычно менее абстрактный, более компактный и более «компьютерный», чем исходный код, эффективность байт-кода обычно выше, чем чистая интерпретация исходного кода, предназначенного для правки человеком.

В составе базовых пакетов R поставляется пакет compiler, который входит состав ядра R, но не загружается по умолчанию при старте R-сессии. Данный пакет включает в себя ряд функций для компиляции R-кода в байт-код.

Параметры компиляции

Все рассмотренные ранее функции из пакета compiler имеют опции, которые могут быть переданы в качестве аргументов функциям компиляции (аргумент options), или заданы глобально с помощью функции setCompilerOptions().

Рассмотрим эти опции:

  • optimize - определяет уровень оптимизации: принимает значения от 0 до 3 (по умолчанию 2);
  • suppressAll - управляет сообщениями: принимает значения TRUE или FALSE (по умолчанию code>FALSE</code>);
  • suppressUndefined - управление сообщения о неопределённых (undefined) переменных: может принимать значения TRUE или список имён переменных (по умолчанию ".Generic", .Method", ".Random.seed", ".self").

Получить текущее значение глобальных опций компиляции можно с помощью функции getCompilerOption():

КодR

<syntaxhighlight lang="r">getCompilerOption("optimize") #> [1] 2 getCompilerOption("suppressAll") #> [1] FALSE getCompilerOption("suppressUndefined") #> [1] ".Generic" ".Method" ".Random.seed" ".self" </syntaxhighlight>

Компиляция функций и выражений

Скомпилировать отдельно взятое выражение можно с помощью функции compile().

КодR

<syntaxhighlight lang="r"># объявляем переменные s <- as.double(0) x <- as.double(1:1000) # объявляем выражения expr <- expression(for (i in x) s <- s + i) exprc <- compile(for (i in x) s <- s + i) # сравниваем результат работы выражений identical(eval(expr), eval(exprc)) #> [1] TRUE # сравниваем производительность выражений microbenchmark(eval(expr), eval(exprc)) #> Unit: nanoseconds #> expr min lq median uq max neval #> eval(expr) 191031 200750 205432 210436 291039 100 #> eval(exprc) 924 1202 1346 1628 11845 100 </syntaxhighlight>

Компиляция функций осуществляется с помощью функции cmpfun(). Возьмём пример из документации к функции cmpfun(), вкотором предлагается сравнить производительность различных реализаций функции lapply(), а также их скомпилированных версий:

КодR

<syntaxhighlight lang="r"># old R version of lapply la1 <- function(X, FUN, ...) { FUN <- match.fun(FUN) if (!is.list(X)) X <- as.list(X) rval <- vector("list", length(X)) for(i in seq(along = X)) rval[i] <- list(FUN(X[[i]], ...)) names(rval) <- names(X) # keep `names' ! return(rval) } # a small variation la2 <- function(X, FUN, ...) { FUN <- match.fun(FUN) if (!is.list(X)) X <- as.list(X) rval <- vector("list", length(X)) for(i in seq(along = X)) { v <- FUN(X[[i]], ...) if (is.null(v)) rval[i] <- list(v) else rval[[i]] <- v } names(rval) <- names(X) # keep `names' ! return(rval) } </syntaxhighlight>

Скомпилируем эти функции в бат-код:

КодR

<syntaxhighlight lang="r">la1c <- cmpfun(la1) la2c <- cmpfun(la2) lapplyc <- cmpfun(lapply) </syntaxhighlight>

Сравним производительность этих функций:

КодR

<syntaxhighlight lang="r">x <- 1:1000 microbenchmark(lapply(x, is.null), la1(x, is.null), la2(x, is.null), lapplyc(x, is.null), la1c(x, is.null), la2c(x, is.null)) #> Unit: microseconds #> expr min lq median uq max neval #> lapply(x, is.null) 173.1 183.2 193.4 205.2 294.4 100 #> la1(x, is.null) 792.3 836.1 861.1 929.9 1807.6 100 #> la2(x, is.null) 965.0 1012.1 1036.6 1085.9 2769.5 100 #> lapplyc(x, is.null) 171.5 182.4 189.1 200.3 265.6 100 #> la1c(x, is.null) 394.2 416.0 428.0 446.9 1217.8 100 #> la2c(x, is.null) 397.6 424.5 439.1 457.2 1216.0 100 </syntaxhighlight>

Обращается на себя внимание, что скомпилированная версия функции lapply() не превосходит по производительности оригинальную версию. Происходит это потому, что функция lapply() использует написанную на C, скомпилированную функцию. Также отметим, что скомпилированные версии функций la1() и la2() показываются практически одинаковую производительность, тогда как нескомпилированные версии довольно сильно различались. Это достигается за счёт высокой оптимизации работы циклов при компиляции функций.

Компиляция скриптов

Ещё одна возможность, предоставляемая пакетом compiler - это компиляция R-скриптов. Создание скомпилированных файлов осуществляется с помощью функции \texttt{cmpfile()}, а загрузка скомпилированных файлов с помощью функции loadcmp(). Если выходной файл не указан, то выходной файл имеет тоже имя, что и входной, но с расширением .Rc. Пример использования:

КодR

<syntaxhighlight lang="r">cmpfile("script.R", "script.Rc") loadcmp("script.Rc") </syntaxhighlight>

JIT-компиляция

Помимо компиляции отдельных функций и выражений, пакет compiler предоставляет возможность JIT-компиляции (JIT - just-in-time) или компиляции «на лету», т.е. во время непосредственного выполнения кода. Переключение режима осуществляется с помощью функции enableJIT(). Эта функции имеет всего один аргумент (level), который может принимать одно из трёх значений:

  • 0 --- отключение JIT-компиляции;
  • 1 --- компиляции функций до их первого вызова;
  • 2 --- тоже что и 2 плюс компиляция всех циклов for, while, repeat до их вызова.

Отметим, что при включении JIT-компиляции при первом запуске выполнения кода компиляция всех функций и циклов займёт некоторое время.

Примечания


Ссылки