CLOS make-instance действительно медленный и вызывает исчерпание кучи в SBCL
Я пишу многоархитектурный ассемблер / дизассемблер в Common Lisp (SBCL 1.1.5 в 64-битном Debian GNU/Linux), в настоящее время ассемблер производит правильный код для подмножества x86-64. Для сборки кода сборки x86-64 я использую хеш-таблицу, в которой мнемоника (строки) инструкции сборки, такая как "jc-rel8"
а также "stosb"
ключи, которые возвращают список из 1 или более функций кодирования, как показано ниже:
(defparameter * emit-function-hash-table-x64 * (make-hash-table: test 'equalp)) (setf (gethash "jc-rel8" * emit-function-hash-table-x64 *) (list # 'jc-rel8-x86)) (setf (gethash "stosb" * emit-function-hash-table-x64 *) (list # 'stosb-x86))
Функции кодирования похожи на эти (хотя некоторые являются более сложными):
(defun jc-rel8-x86 (arg1 и остальные аргументы) (jcc-x64 #x72 arg1)) (defun stosb-x86 (& остальные аргументы) (список №xaa))
Теперь я пытаюсь включить полный набор команд x86-64, используя данные кодирования инструкций NASM (NASM 2.11.06) (файл insns.dat
) преобразован в синтаксис Common Lisp CLOS. Это будет означать замену обычных функций, используемых для выдачи двоичного кода (как функции выше), на экземпляры пользовательских x86-asm-instruction
класс (до сих пор очень простой класс, около 20 слотов с :initarg
, :reader
, :initform
и т. д.), в котором emit
Метод с аргументами будет использоваться для выдачи двоичного кода для заданной инструкции (мнемоники) и аргументов. Преобразованные данные команд выглядят так (но это более 40 000 строк и ровно 7193) make-instance
и 7193 setf
"S).
;; первые экземпляры комбинации мнемоник + операнд (:is-вариант t).;; Есть 4928 таких экземпляров для x86-64, сгенерированных из insns.dat NASM. (eval-when (:compile-toplevel:load-toplevel:execute) (setf Jcc-imm-near (make-instance 'x86-asm-инструкция):name "Jcc": операнды "imm|near": строка кода "[i: odf 0f 80+c rel]":arch-flags (список "386" "BND"): это вариант т)) (setf STOSB-void (make-instance 'x86-asm-инструкция): имя "СТОСБ": операнды "пустота": кодовая строка "[aa]": arch-flags (список "8086"): это вариант т));; затем, экземпляры контейнеров, которые содержат (или могут быть использованы вместо);; возможные варианты каждой инструкции.;; Есть 2265 таких экземпляров для x86-64, сгенерированных из insns.dat NASM. (setf Jcc (make-instance 'x86-asm-инструкция:name "Jcc": is-контейнер т: варианты (список Jcc-imm-near Jcc-imm64-рядом Jcc-IMM-короткая Jcc-немедл Jcc-немедл Jcc-немедл Jcc-IMM))) (setf STOSB (make-instance 'x86-asm-инструкция: имя "СТОСБ": is-контейнер т: варианты (список СТОСБ-пустот)));; тысячи объектов больше здесь...); эта скобка закрывается (eval-when (: compile-toplevel: load-toplevel: execute)
Я конвертировал NASM insns.dat
в общий синтаксис Lisp (как выше) с использованием тривиального скрипта Perl (ниже, но в самом скрипте нет ничего интересного), и в принципе это работает. Так что это работает, но компиляция этих 7193 объектов действительно очень медленная и обычно вызывает исчерпание кучи. На моем ноутбуке Linux Core i7-2760QM с 16 ГБ памяти (eval-when (:compile-toplevel :load-toplevel :execute)
Блок кода с 7193 объектами, подобными приведенным выше, занимает более 7 минут и иногда приводит к исчерпанию кучи, например:
;; Swank запущен в порту: 4005. * Куча исчерпана во время сборки мусора: доступно 0 байт, 32 запрошено. Gen StaPg UbSta LaSta LUbSt В штучной упаковке Unboxed LB LUB! Move Alloc Waste Trig WP GCs Mem-age 0: 0 0 0 0 0 0 0 0 0 0 0 41943040 0 0 0.0000 1: 0 0 0 0 0 0 0 0 0 0 0 41943040 0 0 0,0000 2: 0 0 0 0 0 0 0 0 0 0 0 41943040 0 0 0,0000 3: 38805 38652 0 0 49474 15433 389 416 0 2144219760 9031056 1442579856 0 1 1,5255 4: 127998 127996 0 0 45870 14828 106 143 199 1971682720 25428576 2000000 0 0 0,0000 5: 0 0 0 0 0 0 0 0 0 0 0 2000000 0 0 0,0000 6: 0 0 0 0 1178 163 0 0 0 43941888 0 2000000 985 0 0,0000 Всего выделено байтов = 4159844368 Байт динамического пространства = 4194304000 Управляющие переменные GC: * GC-INHIBIT* = true *GC-PENDING* = в процессе *STOP-FOR-GC-PENDING* = ложная фатальная ошибка, обнаруженная в pid SBCL 9994(tid 46912556431104): куча исчерпана, игра окончена. Добро пожаловать в LDB, низкоуровневый отладчик для среды выполнения Lisp. БОД>
Я должен был добавить --dynamic-space-size 4000
параметр для SBCL, чтобы он компилировался вообще, но все равно после выделения 4 гигабайт динамической кучи иногда исчерпывается. Даже если будет исчерпана куча, более 7 минут для компиляции 7193 экземпляров после добавления только слота в классе ('x86-asm-instruction
класс, используемый для этих случаев) - слишком много для интерактивной разработки в REPL (я использую slimv, если это имеет значение).
Вот (time (compile-file
выход:
; поймал 18636 ВНИМАНИЕ; insns.fasl написано; компиляция завершена в 0:07:11.329 Оценка заняла: 431,329 секунд реального времени 238,317000 секунд общего времени выполнения (234,972000 пользователей, 3,345000 системы) [Время выполнения состоит из 6,073 секунд времени GC и 232,244 секунд времени без GC. ] 55,25% CPU 50 367 интерпретированных форм 784 044 лямбд 1 031 842 900 608 тактов процессора 19,402,921,376 байтов
Использование ООП (CLOS) позволит включить мнемонику команды (такую как jc
или же stosb
выше, :name
), разрешенные операнды инструкции (:operands
) двоичное кодирование инструкции (например, #xaa
за stosb
, :code-string
) и возможные ограничения архитектуры (:arch-flags
) инструкции в одном объекте. Но кажется, что, по крайней мере, мой 3-летний компьютер недостаточно эффективен для быстрой компиляции около 7000 экземпляров объектов CLOS.
Мой вопрос: есть ли способ сделать SBCL make-instance
быстрее, или я должен сохранить генерацию ассемблерного кода в обычных функциях, как в примерах выше? Я также был бы очень рад узнать о любых других возможных решениях.
Вот сценарий Perl, на всякий случай:
#!/usr/bin/env perl
use strict;
use warnings;
# this program converts NASM's `insns.dat` to Common Lisp Object System (CLOS) syntax.
my $firstchar;
my $line_length;
my $are_there_square_brackets;
my $mnemonic_and_operands;
my $mnemonic;
my $operands;
my $code_string;
my $flags;
my $mnemonic_of_current_mnemonic_array;
my $clos_object_name;
my $clos_mnemonic;
my $clos_operands;
my $clos_code_string;
my $clos_flags;
my @object_name_array = ();
my @mnemonic_array = ();
my @operands_array = ();
my @code_string_array = ();
my @flags_array = ();
my @each_mnemonic_only_once_array = ();
my @instruction_variants_array = ();
my @instruction_variants_for_current_instruction_array = ();
open(FILE, 'insns.dat');
$mnemonic_of_current_mnemonic_array = "";
# read one line at once.
while (<FILE>)
{
$firstchar = substr($_, 0, 1);
$line_length = length($_);
$are_there_square_brackets = ($_ =~ /\[.*\]/);
chomp;
if (($line_length > 1) && ($firstchar =~ /[^\t ;]/))
{
if ($are_there_square_brackets)
{
($mnemonic_and_operands, $code_string, $flags) = split /[\[\]]+/, $_;
$code_string = "[" . $code_string . "]";
($mnemonic, $operands) = split /[\t ]+/, $mnemonic_and_operands;
}
else
{
($mnemonic, $operands, $code_string, $flags) = split /[\t ]+/, $_;
}
$mnemonic =~ s/[\t ]+/ /g;
$operands =~ s/[\t ]+/ /g;
$code_string =~ s/[\t ]+/ /g;
$flags =~ s/[\t ]+//g;
# we don't want non-x86-64 instructions here.
unless ($flags =~ "NOLONG")
{
# ok, the content of each field is now filtered,
# let's convert them to a suitable Common Lisp format.
$clos_object_name = $mnemonic . "-" . $operands;
# in Common Lisp object names `|`, `,`, and `:` must be escaped with a backslash `\`,
# but that would get too complicated.
# so we'll simply replace them:
# `|` -> `-`.
# `,` -> `.`.
# `:` -> `.`.
$clos_object_name =~ s/\|/-/g;
$clos_object_name =~ s/,/./g;
$clos_object_name =~ s/:/./g;
$clos_mnemonic = "\"" . $mnemonic . "\"";
$clos_operands = "\"" . $operands . "\"";
$clos_code_string = "\"" . $code_string . "\"";
$clos_flags = "\"" . $flags . "\""; # add first and last double quotes.
$clos_flags =~ s/,/" "/g; # make each flag its own Common Lisp string.
$clos_flags = "(list " . $clos_flags. ")"; # convert to `list` syntax.
push @object_name_array, $clos_object_name;
push @mnemonic_array, $clos_mnemonic;
push @operands_array, $clos_operands;
push @code_string_array, $clos_code_string;
push @flags_array, $clos_flags;
if ($mnemonic eq $mnemonic_of_current_mnemonic_array)
{
# ok, same mnemonic as the previous one,
# so the current object name goes to the list.
push @instruction_variants_for_current_instruction_array, $clos_object_name;
}
else
{
# ok, this is a new mnemonic.
# so we'll mark this as current mnemonic.
$mnemonic_of_current_mnemonic_array = $mnemonic;
push @each_mnemonic_only_once_array, $mnemonic;
# we first push the old array (unless it's empty), then clear it,
# and then push the current object name to the cleared array.
if (@instruction_variants_for_current_instruction_array)
{
# push the variants array, unless it's empty.
push @instruction_variants_array, [ @instruction_variants_for_current_instruction_array ];
}
@instruction_variants_for_current_instruction_array = ();
push @instruction_variants_for_current_instruction_array, $clos_object_name;
}
}
}
}
# the last instruction's instruction variants must be pushed too.
if (@instruction_variants_for_current_instruction_array)
{
# push the variants array, unless it's empty.
push @instruction_variants_array, [ @instruction_variants_for_current_instruction_array ];
}
close(FILE);
# these objects need be created already during compilation.
printf("(eval-when (:compile-toplevel :load-toplevel :execute)\n");
# print the code to create each instruction + operands combination object.
for (my $i=0; $i <= $#mnemonic_array; $i++)
{
$clos_object_name = $object_name_array[$i];
$mnemonic = $mnemonic_array[$i];
$operands = $operands_array[$i];
$code_string = $code_string_array[$i];
$flags = $flags_array[$i];
# print the code to create a variant object.
# each object here is a variant of a single instruction (or a single mnemonic).
# actually printed as 6 lines to make it easier to read (for us humans, I mean), with an empty line in the end.
printf("(setf %s (make-instance 'x86-asm-instruction\n:name %s\n:operands %s\n:code-string %s\n:arch-flags %s\n:is-variant t))",
$clos_object_name,
$mnemonic,
$operands,
$code_string,
$flags);
printf("\n\n");
}
# print the code to create each instruction + operands combination object.
# for (my $i=0; $i <= $#each_mnemonic_only_once_array; $i++)
for my $i (0 .. $#instruction_variants_array)
{
$mnemonic = $each_mnemonic_only_once_array[$i];
# print the code to create a container object.
printf("(setf %s (make-instance 'x86-asm-instruction :name \"%s\" :is-container t :variants (list \n", $mnemonic, $mnemonic);
@instruction_variants_for_current_instruction_array = $instruction_variants_array[$i];
# for (my $j=0; $j <= $#instruction_variants_for_current_instruction_array; $j++)
for my $j (0 .. $#{$instruction_variants_array[$i]} )
{
printf("%s", $instruction_variants_array[$i][$j]);
# print 3 closing brackets if this is the last variant.
if ($j == $#{$instruction_variants_array[$i]})
{
printf(")))");
}
else
{
printf(" ");
}
}
# if this is not the last instruction, print two newlines.
if ($i < $#instruction_variants_array)
{
printf("\n\n");
}
}
# print the closing bracket to close `eval-when`.
print(")");
exit;
1 ответ
18636 предупреждений выглядит очень плохо, Начните с избавления от всех предупреждений.
Я бы начал с избавления от EVAL-WHEN
вокруг всего этого. Не имеет особого смысла для меня. Либо загрузите файл напрямую, либо скомпилируйте и загрузите файл.
Также обратите внимание, что SBCL не нравится (setf STOSB-void ...)
когда переменная не определена. Новые переменные верхнего уровня вводятся с DEFVAR
или же DEFPARAMETER
, SETF
просто устанавливает их, но не определяет их. Это должно помочь избавиться от предупреждений.
Также :is-container t
а также :is-variant t
Запах, как эти свойства должны быть преобразованы в классы для наследования (например, как mixin). Контейнер имеет варианты. Вариант не имеет вариантов.