Опубликован: 19.04.2025 | Доступ: свободный | Студентов: 0 / 0 | Длительность: 01:25:00
Лекция 3:

Покрытие деревьев

< Лекция 2 || Лекция 3 || Лекция 4 >

Одним из основных ограничений раскрытия макросов является то, что в нём порождается код, рассматривая только одну инструкцию или только один узел промежуточного представления. Из-за этого порождается код плохого качества. Другой сложностью является то, что поиск кандидатов в порожденный код и выбор наилучшего осуществляется за один шаг, что делает задачу исследования разных комбинаций инструкций затруднительной. Эти недостатки решает порождение кода с помощью деревьев.

Суть идеи заключается в том, что нам дано некоторое дерево, которое представляется собой программы, а также некоторый шаблон древовидных шаблонов (англ. pattern). Задача порождения кода сводится к задаче покрытия нашего дерева подмножеством этих шаблонов оптимальным образом, т.е. задача разбивается на поиск всех возможных покрытий и выбор оптимального покрытия шаблонами-образцами. Для большинства архитектур шаблоны будут пересекаться, и поэтому различных покрытий будет много. Обычно, мы будет стараться воспользоваться минимальным количеством шаблонов:

  • Предпочитая крупные шаблоны мы будет использовать специализированные инструкции, которые, как правило, исполняются быстрее.
  • С меньшим количеством шаблонов они будут меньше пересекаться, а значит меньше данных будет пересчитываться заново, что приведет к улучшению производительности и размера кода.

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

x = A[i + 1];

Пример: простое выражение, которое загружает по индексу i+1 из массива чисел A. Предполагается, что индекс i находится в регистре, A --- в памяти, а числе 8байтные. Всего три полных покрытия дерева шаблонами: \{ m_1, \dots, m_7, m_9 \}, \{ m_1, \dots, m_5, m_8, m_9 \} и \{ m_1, \dots, m_5, m_{10} \},

mv r <- var
add r <- s + t
mul r <- s ? t
muladd r <- s ? t + u
load r <- ?s
maload r <- ?(s ? t + u)

Дерево выражений и его покрытие шаблонами

% !TEX TS-program = xelatex
% !TeX spellcheck = ru_RU
% !TEX root = sel2covering.tex

\documentclass[tikz,border=3.14mm]{standalone}

\xdefinecolor{mycolor}{RGB}{62,96,111} % Neutral Blue
\colorlet{bancolor}{mycolor}

\usepackage{tikz}
\usetikzlibrary{arrows,backgrounds,calc,trees}

\pgfdeclarelayer{background}
\pgfsetlayers{background,main}


\newcommand{\convexpath}[2]{
    [
    create hullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename) [draw=none,name=hullnode\counter] {};
        }
        \node at (hullnode\numberofnodes) [name=hullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hullnode1) [name=hullnode\lastnumber,draw=none] {};
    },
    create hullnodes
    ]
    ($(hullnode1)!#2!-90:(hullnode0)$)
    \foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
        let
        \p1 = ($(hullnode\currentnode)!#2!-90:(hullnode\previousnode)$),
        \p2 = ($(hullnode\currentnode)!#2!90:(hullnode\nextnode)$),
        \p3 = ($(\p1) - (hullnode\currentnode)$),
        \n1 = {atan2(\y3,\x3)},
        \p4 = ($(\p2) - (hullnode\currentnode)$),
        \n2 = {atan2(\y4,\x4)},
        \n{delta} = {-Mod(\n1-\n2,360)}
        in
        {-- (\p1) arc[start angle=\n1, delta angle=\n{delta}, radius=#2] -- (\p2)}
    }
    -- cycle
}

\begin{document}
\thispagestyle{empty}

\begin{tikzpicture}[every tree node/.style={draw,circle}
    , sibling distance=25pt
    , level distance=30pt
    , leaf/.style={circle,fill=blue,fill opacity=0.3,text opacity=1}
    , dot/.default = 6pt % size of the circle diameter
    ]
\node at (-1,-1) (labM10) {$m_{10}$};
\node at (1,-1.5) (labM8) {$m_{8}$};
        \node (m9) {ld} {
        child { node (m6) {$+$}
            child { node (m7) {$\times$}
                child { node[leaf] (g) {+}
                    child { node[leaf] (a) {i}
                    }
                    child { node[leaf] (b) {1}
                    }
                }
                child { node[leaf] (m3) {8} }
            }
            child { node[leaf] (m4) {A} } } };
\begin{pgfonlayer}{background}
%   \fill[red,opacity=0.3] \convexpath{a,g,b}{8pt};
    \fill[red,opacity=0.3] \convexpath{m6,m7}{8pt};
    \fill[blue,opacity=0.3] \convexpath{m6,m7,m9}{10pt};
%    \fill[blue,opacity=0.3] \convexpath{m3,m3}{10pt};
\end{pgfonlayer}
%\draw (labM10) --  (0,0);
\draw[blue] (labM8) --  (.20,-1.3);
\node at (0,-3.7) (labM3) {$m_3$};
\node at (-0.3,-4.9) (labM1) {$m_1$};
\node at (-1.3,-4.9) (labM2) {$m_2$};
\node at (.7,-2.6) (labM4) {$m_4$};
\node at (-1.5,-3.1) (labM5) {$m_5$};
\node at (-1,-2.1) (labM7) {$m_7$};
\node at (0.7,-.1) (labM9) {$m_9$};
\end{tikzpicture}


\end{document}

Использование синтаксического анализа

В попытке преодолеть "наколеночность" методов с раскрытием макросов, были предложены подходы к выбору инструкций с использованием формализмов. Одним из них может быть использование формальных грамматик и подходов на основе синтаксического анализа языков. Было предложено :cite:`GlanvilleGraham1978` описывать промежуточное представление программы с помощью контекстно-свободных грамматик, где правила аргументирована стоимостью операций и некоторым действием (англ. action code), которое будет заниматься непосредственно порождением кода.

Грамматика для порождения кода для арифметических выражений
Инструкция Стоимость Действие
r1 <- r1 + r2 1 emit add r1,r1,r2
r1 <- r1 ? r2 1 emit mul r1,r1,r2
r3 <- Int 1 emit li r1, I

В грамматике используются так называемые терминальные символы (в нашем примере названия арифметических действий и числа), и нетерминальные символы (названия регистров-локаций)

Пример выражения, для которого будем порождать инструкции с помощью синтаксического анализа


Рис. 3.1.

Во время анализа на стеке накапливаются текущие терминалы и нетерминалы. После получения нового терминала и добавления его на стек, анализатор может сделать два действия:

  • shift --- продолжить чтение терминалов и оставив стек без изменений;
  • reduce --- выбрать правило грамматики, снять с вершины стека нетерминалы из правой части правила, и заменить на левую часть правила; вместе с этим сгенерировать некоторый код на ассемблере.

Таким образом для входа a+b*c, где a,b,c --- целые числа, мы породим примерно такой код, совершив следующие действия: s\ r_3\ s\ s\ r_3\ s\ s\ r_3\ r_2\ r_1, где s --- shift, а r_N --- reduce по правилу N.

li  R1, a
li  R2, b
mul R1, R1, R2
li  R3, c
add R1, R1, R3

В правилах у регистров есть индексы, которые позволяют выражать случаи, когда вход и выход инструкций приходятся на один и тот же регистр.

Основной сложностью такого вида синтаксического анализа, является то, что не всегда очевидно, когда предпочитать shift, а когда reduce. Обычно это решается переписыванием грамматики так, чтобы конфликтные случаи не случались. Но для больших грамматик делать это вручную затруднительно. В изначальном подходе конфликт между shift и reduce всегда разрешался в пользу shift, а если на стеке получалось слишком много терминалов, то применялись ad hoc правила, чтобы сгенерировать код как-нибудь и исправить (почти) аварийное состояние. В случае reduce/reduce конфликта, выглядит разумным пытаться применить самое длинное правило. (Случаи, когда два правила одинаковой длины конфликтуют, можно задетектировать до запуска синтаксического анализа.)

Преимущества. В процессе синтаксического анализ снизу вверх строится таблица состояний с переходами, которая позволяет вести анализ за время пропорциональное размеру входа. Также такой вид синтаксического анализа выступает в роли формальной теории, чтобы, например, обосновывать полноту грамматики инструкций

Недостатки. Во-первых, из-за использования грамматик в момент синтаксического анализа мы не имеем доступа к конкретным значениям, например, констант. Из-за этого невозможно выразить какие-то ограничения на диапазоны констант и т.п. Так же, если инструкции имеют много видов адресации операндов (эта проблема должна обойти RISC-V стороной), то появляется много похожих правил, специализированных под местонахождение операндов. Так для CISC архитектуры VAX, грамматика разрослась до миллионов правил :cite:`VAX1982`. Методы рефакторинга и упрощения грамматик известны, но их в данном случае надо применять с осторожностью, чтобы не повредить качеству порождаемого кода.

В контексте RISC-V можно привести такой пример. Существуют расширения, которые позволяют сделать сложение-со-сдвигом, c помощью них можно реализовать умножение на некоторые константы. Например, можно mul r0, r1, 9 заменить на sh3add r0, r1, r1, за счет соотношения r*9 = r + r lsl 3.

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

Порождение кода путём анализа сверху вниз

Анализ сверху вниз вначале выбирает правило порождения кода, а уже потом проталкивает вниз все необходимые ограничения для операндов паттерна. Таким образом можно выражать, например, ограничения на константы, которые участвуют в операндах. При выборе правила можно не угадать, что приведет к невозможности породить код для операндов. В этих случаях процесс возвращается назад (англ. backtracking) и пробует применить другое правило. К сожалению большое количество возвратов назад, негативно влияет на производительности, из-за чего и первые испытания такого подхода :cite:`Newcomer1975`, и последующие :cite:`Nymeyer1996` не сыскали широкого распространения.

Отличительной чертой подходов сверху вниз является сопоставление представления программы с шаблонами с учетом некоторых аксиом (например, not (E1<=E2) заменяется на E1>E2, E+0 на E, и т.п.), чтобы получать более эффективный результат.

Отделение сопоставления с образцами-шаблонами и порождения кода

В предыдущих подходах выбор шаблонов и порождение кода делались вместе, что позволяет порождать код за один проход и получать более быстрый компилятор. Но при этом, при порождении кода сложно учесть влияние разных комбинаций шаблонов. Поэтому можно исследовать идею разделения фаз покрытия дерева образцами-шаблонами и порождение кода по этим шаблонам.

В литературе также встречаются исследования по оптимизации поиска подходящих шаблонов для дерева. Они заключаются в сведении задачи сопоставления с образцом к задаче поиска подстроки в строке :cite:`AhoCorasik1975`, также построение таблиц для сопоставления с образцом, и последующее сжатие их. Основным достижением этих подходов является поиск всех возможных корректных сочетаний шаблонов за линейное время от размера программы. В данном документе они не освещены.

Динамическое программирование

С появлением возможности получения всех подходящих сочетаний шаблонов за линейное время, начали появляться идеи выполнения выбора инструкций также за линейное время. Первые идеи :cite:`Ripken1977` использования динамического программирования позже привели к появлению генератора компиляторов Twig :cite:`Aho1989`, которые принимал на вход описание архитектуры на языке CGL (Code Generator Language) и дерево компилируемой программы, и порождал код за три прохода.

  • Проход сверху вниз, который находил для каждого узла дерева множества подходящих шаблонов.
  • Снизу вверх вычислялась стоимость выбора соответствующего шаблона для каждого узла.
  • Последний проход сверху вниз выбирал покрытие наименьшей стоимости, и по дороге порождал код.

Такой подход имеет преимущества, по сравнению с подходом на основе синтаксического LR анализа. Основным является то, что конфликты теперь сами разрешаются путём вычисления стоимости применения конкретных шаблонов. Также описания шаблонов для архитектур становятся существенно короче.

К сожалению, подход динамического программирования предполагает, что задача может быть разбита на подзадачи, которые могут быть решены оптимально по-отдельности, и потом скомбинированы. На практике, задача порождения кода не обладает такими свойствами, так как последующие фазы работы компилятора --- переупорядочивание инструкций (instruction scheduling) и распределение регистров (англ. register allocation) --- способны оказать существенный эффект на производительность порожденного кода.

Ограничения покрытия деревьев

Основным недостатком работы с деревьями выражений является то, что одинаковые подвыражения должны быть разделены по рёбрам и продублированы при построении дерева. Такие преобразования известны в литературе как edge splitting и node duplication. В зависимости от набора инструкций, не разделяя подвыражения можно добивать лучшего качества кода.

В примере ниже общее выражение для вычисления значения t было разделено, что приводит к покрытию m_1,...,m_7,m_9 со стоимостью 0+...+0+2+3+5=10. Если представить дерево как граф без циклов, то его можно покрывать шаблонами m_8 и m_{10}, что даст стоимость 0+...+0+4+5=9.

Пример. Инструкции и их стоимость. Нотация *s означает получения данных по адресу в памяти.

Инструкция Стоимость
add r <- s + t 2
mul r <- s x t 3
addmul r <- (s + t) ? u 4
load r <- * s 5
addload r <- * (s + t) 5
t = a + b;
x = c * t;
y = *(( int *) t);

Деревья выражений после совершения деления рёбер (англ. edge splitting).

% !TEX TS-program = xelatex
% !TeX spellcheck = ru_RU
% !TEX root = sel2dag0.tex

\documentclass[tikz,border=3.14mm]{standalone}

\xdefinecolor{mycolor}{RGB}{62,96,111} % Neutral Blue
\colorlet{bancolor}{mycolor}

\usepackage{tikz}
\usetikzlibrary{arrows,backgrounds,calc,trees}

\pgfdeclarelayer{background}
\pgfsetlayers{background,main}


\newcommand{\convexpath}[2]{
    [
    create hullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename) [draw=none,name=hullnode\counter] {};
        }
        \node at (hullnode\numberofnodes) [name=hullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hullnode1) [name=hullnode\lastnumber,draw=none] {};
    },
    create hullnodes
    ]
    ($(hullnode1)!#2!-90:(hullnode0)$)
    \foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
        let
        \p1 = ($(hullnode\currentnode)!#2!-90:(hullnode\previousnode)$),
        \p2 = ($(hullnode\currentnode)!#2!90:(hullnode\nextnode)$),
        \p3 = ($(\p1) - (hullnode\currentnode)$),
        \n1 = {atan2(\y3,\x3)},
        \p4 = ($(\p2) - (hullnode\currentnode)$),
        \n2 = {atan2(\y4,\x4)},
        \n{delta} = {-Mod(\n1-\n2,360)}
        in
        {-- (\p1) arc[start angle=\n1, delta angle=\n{delta}, radius=#2] -- (\p2)}
    }
    -- cycle
}

\begin{document}
\thispagestyle{empty}

\begin{tikzpicture}[every tree node/.style={draw,circle}
    , sibling distance=25pt
    , level distance=30pt
    , leaf/.style={circle,fill=blue,fill opacity=0.3,text opacity=1}
    , dot/.default = 6pt % size of the circle diameter
    ]
\node[leaf] at ( 0, 0) (M7) {$\times$};
\node[leaf] at ( 1,-1) (M4) {$t$};
\node[leaf] at (-1,-1) (M3) {$c$};
\node[leaf] at (-2,-1) (M2) {$b$};
\node[leaf] at (-3, 0) (M6) {$+$};
\node[leaf] at (-4,-1) (M1) {$a$};
\node[leaf] at ( 2, 0) (M9) {$ld$};
\node[leaf] at ( 2,-1) (M5) {$t$};
\begin{pgfonlayer}{background}
%   \fill[red,opacity=0.3] \convexpath{a,g,b}{8pt};
%    \fill[red,opacity=0.3] \convexpath{m6,m7}{8pt};
%    \fill[blue,opacity=0.3] \convexpath{m6,m7,m9}{10pt};
%    \fill[blue,opacity=0.3] \convexpath{m3,m3}{10pt};
\end{pgfonlayer}
\draw[->,thick] (M1) --  (M6);
\draw[->,thick] (M2) --  (M6);
\draw[->,thick] (M3) --  (M7);
\draw[->,thick] (M4) --  (M7);
\draw[->,thick] (M5) --  (M9);
%\draw[blue] (labM8) --  (.20,-1.3);
\node at (-4,-2) (labM1) {$m_1$};
\node at (-2,-2) (labM2) {$m_2$};
\node at (-1,-2) (labM3) {$m_3$};
\node at ( 1,-2) (labM4) {$m_4$};
\node at ( 2,-2) (labM5) {$m_5$};
\node at (-4, 0) (labM6) {$m_6$};
\node at (-1, 0) (labM7) {$m_7$};
\node at ( 3, 0) (labM9) {$m_9$};
\end{tikzpicture}

\end{document}

Представление программы в виде графа без циклов (вместо деревьев).

% !TEX TS-program = xelatex
% !TeX spellcheck = ru_RU
% !TEX root = sel2dag1.tex

\documentclass[tikz,border=3.14mm]{standalone}

\xdefinecolor{mycolor}{RGB}{62,96,111} % Neutral Blue
\colorlet{bancolor}{mycolor}

\usepackage{tikz}
\usetikzlibrary{arrows,backgrounds,calc,trees}

\pgfdeclarelayer{background}
\pgfsetlayers{background,main}


\newcommand{\convexpath}[2]{
    [
    create hullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename) [draw=none,name=hullnode\counter] {};
        }
        \node at (hullnode\numberofnodes) [name=hullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hullnode1) [name=hullnode\lastnumber,draw=none] {};
    },
    create hullnodes
    ]
    ($(hullnode1)!#2!-90:(hullnode0)$)
    \foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
        let
        \p1 = ($(hullnode\currentnode)!#2!-90:(hullnode\previousnode)$),
        \p2 = ($(hullnode\currentnode)!#2!90:(hullnode\nextnode)$),
        \p3 = ($(\p1) - (hullnode\currentnode)$),
        \n1 = {atan2(\y3,\x3)},
        \p4 = ($(\p2) - (hullnode\currentnode)$),
        \n2 = {atan2(\y4,\x4)},
        \n{delta} = {-Mod(\n1-\n2,360)}
        in
        {-- (\p1) arc[start angle=\n1, delta angle=\n{delta}, radius=#2] -- (\p2)}
    }
    -- cycle
}

\begin{document}
\thispagestyle{empty}



\begin{tikzpicture}[every tree node/.style={draw,circle}
    , sibling distance=25pt
    , level distance=30pt
    , leaf/.style={circle,fill=blue,fill opacity=0.3,text opacity=1}
    , dot/.default = 4pt % size of the circle diameter
    %, ->/.style={thick,->}
    ]
    \node[leaf] at (-1,-1) (M7) {$\times$};
    \node[leaf] at ( 1,-1) (M9) {$ld$};
    \node[leaf,minimum size=9pt] at ( 0,-2) (M6) {$+$};
    \node[leaf] at (-1,-3) (M1) {$a$};
    \node[leaf] at ( 1,-3) (M2) {$b$};
    \node[leaf] at (-2,-2) (M3) {$c$};
    \begin{pgfonlayer}{background}
        \fill[red,opacity=0.3] \convexpath{M6,M7}{8pt};
        \fill[red,opacity=0.7] \convexpath{M6,M9}{8pt};
    \end{pgfonlayer}
    \node at ( 1.2,-2) (labM10) {$m_{10}$};
    \draw[red,opacity=0.7] (labM10) --  (.8,-1.5);
    \node at ( -.2,-1) (labM8) {$m_8$};
    \draw[red,opacity=0.7] (labM8) --  (-.28,-1.1);
    \node at (1.8,-1) (labM7) {$m_9$};
    \node at (-1.8,-1) (labM7) {$m_7$};
    \node at (-2,-2.5) (labM3) {$m_3$};
    \node at (-.3,-3) (labM1) {$m_1$};
    \node at (1.8,-3) (labM2) {$m_2$};

\draw[->,thick] (M1) --  (M6);
\draw[->,thick] (M2) --  (M6);
\draw[->,thick] (M3) --  (M7);
\draw[->,thick] (M6) --  (M7);
\draw[->,thick] (M6) --  (M9);

\end{tikzpicture}



\begin{tikzpicture}[every tree node/.style={draw,circle}
    , sibling distance=25pt
    , level distance=30pt
    , leaf/.style={circle,fill=blue,fill opacity=0.3,text opacity=1}
    , dot/.default = 6pt % size of the circle diameter
    ]
\node[leaf] at ( 0, 0) (M7) {$\times$};
\node[leaf] at ( 1,-1) (M4) {$t$};
\node[leaf] at (-1,-1) (M3) {$c$};
\node[leaf] at (-2,-1) (M2) {$b$};
\node[leaf] at (-3, 0) (M6) {$+$};
\node[leaf] at (-4,-1) (M1) {$a$};
\node[leaf] at ( 2, 0) (M9) {$ld$};
\node[leaf] at ( 2,-1) (M5) {$t$};
\begin{pgfonlayer}{background}
%   \fill[red,opacity=0.3] \convexpath{a,g,b}{8pt};
%    \fill[red,opacity=0.3] \convexpath{m6,m7}{8pt};
%    \fill[blue,opacity=0.3] \convexpath{m6,m7,m9}{10pt};
%    \fill[blue,opacity=0.3] \convexpath{m3,m3}{10pt};
\end{pgfonlayer}
\draw[->,thick] (M1) --  (M6);
\draw[->,thick] (M2) --  (M6);
\draw[->,thick] (M3) --  (M7);
\draw[->,thick] (M4) --  (M7);
\draw[->,thick] (M5) --  (M9);
%\draw[blue] (labM8) --  (.20,-1.3);
\node at (-4,-2) (labM1) {$m_1$};
\node at (-2,-2) (labM2) {$m_2$};
\node at (-1,-2) (labM3) {$m_3$};
\node at ( 1,-2) (labM4) {$m_4$};
\node at ( 2,-2) (labM5) {$m_5$};
\node at (-4, 0) (labM6) {$m_6$};
\node at (-1, 0) (labM7) {$m_7$};
\node at ( 3, 0) (labM9) {$m_9$};
\end{tikzpicture}

\end{document}

Также деревья ограничивают разнообразие поддерживаемых инструкций процессора. Так как у деревьев всегда один корень, инструкции с большим количеством выходов (англ. multi-output instructions) не представимы, так как требуют больше одного корня. Даже инструкции с не пересекающимися выходами непредставимы, так как алгоритм выбора инструкций рассматривает деревья по одному.

В-третьих, представление с помощью деревьев не может моделировать граф потока управления. Цикл for требует циклический путь в графе, что не ложится в деревья. По этой причине представление с помощью деревьев годится только для выбора инструкций внутри базового блока (англ. basic block) графа потока управления. Это не позволяет выбирать инструкции процессора, которые соответствуют коду сразу в нескольких базовых блоках, что может негативно влиять на производительность.

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

< Лекция 2 || Лекция 3 || Лекция 4 >