Подробное объяснение виртуальной машины Lua

задняя часть программист Тенсент Lua

Приветствую всех вОблако Tencent + сообщество, получить больше крупной технической практики Tencent по галантерее~

Автор: Чжэн Сяохуэй | Старший инженер по разработке игрового клиента Tencent

Спереди написано: весь текст в этой статье набран мной вручную, а демонстрационный код, представленный далее в этой статье, написан мной построчно.До меня многие старшеклассники изучали виртуальную машину Lua, так что это article На плечи этих гигантов, должно быть, легло много мыслей.

Название этой статьи «Объясните виртуальную машину Lua простыми словами», на самом деле основное внимание уделяется этим двум словам. Ведь технический уровень автора ограничен. Но я слышал, что название должно быть немного похоже на статью, прежде чем кто-то ее прочитает, отсюда и название.

Я посвящаю эту статью тем, кто интересуется виртуальной машиной Lua. Я надеюсь, что эта статья поможет вам добиться эффекта бросания кирпича и привлечения нефрита.

Поток выполнения Lua:

Весь поток кода Lua:

Как показано на рисунке ниже: Программист кодирует файл lua -> синтаксический и лексический анализ создает файл байт-кода Lua (соответствующий Luac.exe из цепочки инструментов Lua) -> виртуальная машина Lua анализирует байт-код и выполняет набор инструкций -> Выводит результат.

В этой статье мы попытаемся рассказать о синих и зеленых частях.

Лексический синтаксический анализ:

Я не буду рассказывать обо всем процессе лексического анализа Lua, ведь если я потрачу слишком много времени на написание этого, я запланирую, что мои одноклассники будут спрашивать меня о ходе разработки моих потребностей, так что сделать длинный Короче говоря, я буду использовать свое понимание Lua.Возьмем конкретный пример для анализа:

Блок кода Lua:

If a < b then a = c end

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

Чтобы компьютер понял это предложение, первое, что нам нужно сделать, это сегментировать слова: поскольку вы не можете его понять. Сначала я разобью предложение на слова и скажу вам, что означает каждое слово.

Результат сегментации слов примерно следующий:

    分词结果         类型(意义)
​
    if             Type_If  (if 关键字)
​
    a             Type_Var (这是一个变量)
​
    <             Type_OpLess(这是一个小于号)
​
    b             Type_Var(这是一个变量)
​
    then          Type_Then(Then关键字)
​
    a             Type_Var (这是一个变量)
​
    =             Type_OpEqual(这是一个等号)
​
    c             Type_Var(这是一个变量)
​
    end           Type_End(End关键字)

Хорошо. Теперь компьютер, наконец, понимает. Оказывается, в написанной вами строчке кода 9 слов, и я понимаю значение каждого слова. Итак, теперь вопрос в том, понимает ли компьютер это предложение?

Компьютеры до сих пор не понимают. Так же, как и предложение «есть», компьютер понимает, что «есть» — это глагол, означающий «открыть рот». «Рис» — это существительное, означающее рис. Но вы готовите еду, а компьютер не знает, что значит «открыть рот, положить еду в рот и проглотить ее желудком». Поскольку компьютер знает только две вещи, "открой рот" и "рис", какая связь между этими двумя вещами, компьютер понять не может. Некоторые люди скажут: просто: есть + другие слова.Эта структура позволяет компьютеру в целом понять смысл помещения предмета, представленного последним словом, в рот, верно? Это касается слова «есть», но как заставить компьютер понимать слово «удивлен»? Итак, вот следующая тема: семантический разбор.

Что касается семантического анализа, если вы хотите узнать больше, вы можете узнать об AST (абстрактном синтаксическом дереве). Однако для нашего примера мы можем понять это простым способом.

Для Lua каждое ключевое слово имеет свою особую структуру. Таким образом, ключевые слова Lua станут объектом семантического анализа. Пример if, в котором мы сейчас задействованы: мы можем просто выразить этот процесс синтаксического анализа в псевдокоде:

Для оператора if мы можем абстрагировать его в эту структуру:

Если условие (условное выражение), то dosth (блок операторов) end

Таким образом, псевдокод для разбора блока операторов if выглядит следующим образом:

       ReadTokenWord();
       If(tokenWord.type == Type_If) then
          ReadCondition()   //读取条件表达式
          ReadThen()       //读取关键字then
          ReadCodeBlock()   //读取逻辑代码块
          ReadEnd()        //读取关键字End
      End

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

Поскольку я просто делаю демонстрацию, я использую предварительные знания. То есть я предполагаю, что логическая структура нашего блока операторов If такова:

Если меньше условного выражения, то выражение присваивания End

Итак, в моей демонстрации структура данных, преобразованная в C++, — это IfStateMent, которая, вероятно, выглядит так:

Итак, теперь весь наш лексический разбор завершен. Но настоящая виртуальная машина Lua не может выполнить наш ifStateMent. Реализация в исходном коде Lua аналогична этому TokenType и структурирована, если оператор whileStatement и т. д., и Lua не генерирует полное синтаксическое дерево. В реализации исходного кода Lua он анализирует некоторые предложения, генерирует временное синтаксическое дерево, а затем переводит его в набор инструкций. Он не ждет, пока все предложения будут проанализированы перед переводом. Семантический анализ и перевод в наборы инструкций — это параллельный процесс. Вставьте частичную реализацию семантического разбора в исходный код:

Хорошо, теперь мы превратили код Lua, введенный нашим программистом, в структуру данных (которую компьютер может понять). Следующий шаг — превратить эту структуру данных во что-то, что сможет распознать виртуальная машина Lua.Это набор инструкций Lua!

Что касается процесса преобразования, то для нашего примера он, вероятно, будет таким:

    If a < b then a = c end

Сначала поймите условие a

а и b являются переменными. Предположим, что наши доступные значения индекса регистра начинаются с 10 (регистры 0-9 уже заняты): и предположим, что у нас есть таблица константного индекса: константа 0: символ 'a', константа 1: строка 'b'. Тогда a

  • LoadK 10, 0: Загрузить _G[ConstVar[0]] в регистр 10: R[10] = _G["a"]

  • LoadK 11, 1: Загрузить _G[ConstVar[1]] в регистр 11: R[11] = _G["b"]

  • LT 10,11 : Сравните, установлено ли R[10]

    Таким же образом продолжайте переводить a=c и так далее.

    Итак, если a

Хорошо, теперь мы, вероятно, понимаем, как код Lua становится набором инструкций.

Теперь давайте взглянем на набор инструкций Lua5.1:

Набор инструкций Lua имеет фиксированную длину, каждая инструкция имеет длину 32 бита, что примерно соответствует этой длине:

Младшие шесть битов каждой инструкции представляют собой код инструкции, например, 0 для MOVE и 12 для сложения. Всего в Lua 37 инструкций, которые

MOVE,LOADK,LOADBOOL,LOADNIL,GETUPVAL,GETGLOBAL,GETTABLE,SETGLOBAL,SETUPVAL,SETTABLE,NEWTABLE,SELF,ADD,SUB,MUL,DIV,MOD,POW,UNM,NOT,LEN,CONCAT,JMP,EQ,LT, LE,TEST,TESTSET,CALL,TAILCALL,RETURN,FORLOOP,TFORLOOP,SETLIST,CLOSE,CLOSURE,VARARG.

Мы обнаружили, что на графике также присутствуют iABC, iABx, iAsBx. Это означает, что некоторые инструкции имеют формат OPCODE, A, B, C, некоторые инструкции имеют формат OPCODE A, BX, а некоторые — формат OPCODE A, sBX. Разница между sBx и bx заключается в том, что bx — целое число без знака, а sbx — число со знаком, т. е. sbx может быть отрицательным.

Я не буду подробно останавливаться на каждой инструкции, но приведу пример:

Код инструкции 0x 00004041 Как разобрать эту инструкцию:

0x4041 = 0000 0000 0000 0000 0100 0000 0100 0001

Младшие шесть битов (0~5) являются кодом операции: 000001 = 1 = инструкция LoadK (0~37 соответствуют 38 инструкциям, которые я перечислил выше, в следующем порядке: 0 — Move, 1 — loadk, 2 — loadbool.. ... 37 — варарг). Формат инструкции LoadK - это формат iABC (C не используется, полезен только ab). Итак, продолжаем читать аб.

a = младшие 6~13 бит равны 00000001 = 1, поэтому a=1

b = младшие 14~22 бита равны 000000001 = 1, поэтому b=1

Итак, 0x4041 = ЗАГРУЗКАK 1, 1

Как разобрать код инструкции я тоже написал в демке, код наверное такой:

Затем байт-код Lua, сгенерированный файлом Lua, скомпилированным Luac, что еще содержит файл байт-кода Lua, кроме набора инструкций? Конечно, он не будет таким умственно отсталым, как показанная выше демонстрация разбора лексической грамматики. Итак, давайте поговорим о структуре файла байт-кода Lua:

Файл байт-кода Lua (*.lua.bytes) содержит: заголовок файла + функцию верхнего уровня:

Структура заголовка файла:

Функции верхнего уровня имеют ту же структуру, что и обычные функции:

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

Пример в демонстрации — это задействованный исходный код Lua, а информация, полученная при окончательном разборе байт-кода, следующая:

Хорошо, последняя часть этой статьи: как виртуальная машина Lua выполняет эти инструкции?

Вероятно, что-то вроде этого:

    While(指令不为空)
       执行指令
       取下一条要执行的指令
    End

Как должна выполняться каждая инструкция? ? ? Если у вас осталось впечатление, после завершения семантического анализа предыдущей статьи набор инструкций по переносу выглядит следующим образом:

a < b

  • LoadK 10, 0: Загрузить _G[ConstVar[0]] в регистр 10: R[10] = _G["a"]

  • LoadK 11, 1: Загрузить _G[ConstVar[1]] в регистр 11: R[11] = _G["b"]

  • LT 10,11 : Сравните, установлено ли R[10]

    Разумеется, текст после инструкции уже подробно описал логику выполнения инструкции, хе-хе.

    Для фактического выполнения нам нужно 1 в дизайне структуры данных, регистр: 2, таблица констант: 3, таблица глобальных переменных:

Чтобы иметь возможность выполнить пример в нашей демонстрации:

Я реализовал все инструкции, задействованные в этом коде

insExecute[(int)OP_LOADK] = &LuaVM::LoadK;
insExecute[(int)OP_SETGLOBAL] = &LuaVM::SetGlobal;
insExecute[(int)OP_GETGLOBAL] = &LuaVM::GetGlobal;
insExecute[(int)OP_ADD] = &LuaVM::_Add;
insExecute[(int)OP_SUB] = &LuaVM::_Sub;
insExecute[(int)OP_MUL] = &LuaVM::_Mul;
insExecute[(int)OP_DIV] = &LuaVM::_Div;
insExecute[(int)OP_CALL] = &LuaVM::_Call;
insExecute[(int)OP_MOD] = &LuaVM::_Mod;
insExecute[(int)OP_LT] = &LuaVM::_LT;
insExecute[(int)OP_JMP] = &LuaVM::_JMP;
insExecute[(int)OP_RETURN] = &LuaVM::_Return;

Возьмите Добавить в качестве примера:

bool LuaVM::_Add(LuaInstrunction ins)
{
    //R(A):=RK(B)+RK(C) :::
    //Todo:必要的参数合法性检查:如果有问题则抛异常  
    // 将ins.bValue代表的数据和ins.cValue代表的数据相加的结果赋值给索引值为ins.aValue的寄存器
    luaRegisters[ins.aValue].SetValue(0, GetBK(ins.bValue) + GetBK(ins.cValue));
    return true;
}

Ниже приведен снимок экрана с эффектом запуска программы:

Прочитав весь процесс, вы можете задуматься над этим вопросом: почему эффективность выполнения Lua намного ниже, чем у программ на C?

Личное скромное мнение:

  1. Истинные и ложные регистры: регистры, участвующие в наборе инструкций Lua, являются симулированными регистрами, и их сущность по-прежнему представляет собой часть данных в памяти. Скорость доступа зависит от того, насколько быстро процессор может получить доступ к памяти. Наконец, программа C может быть выполнена с набором инструкций win32 или набором инструкций Arm. Участвующие в этом регистры EBX, ESP и т.д. — это все гейты И-НЕ на ЦП, и их скорость доступа = частоте ЦП (по сравнению со скоростью обращения ЦП к памяти, а это почти небо и земля).

  2. Платформа, на которой работает набор инструкций: Платформа, на которой работает набор инструкций Lua, — это виртуальная машина Lua. Набор инструкций программы C напрямую поддерживается аппаратным обеспечением.

  3. Данные в C напрямую соответствуют адресу памяти. Данные в Lua соответствуют структуре данных, которая описывает данные. Поэтому после такого слоя КПД сильно снижается.

  4. Например, операция Gc в Lua и т. д. — это все то, что программам на C делать не нужно. . . .

Хорошо, наконец, я представляю исходный код демо, которое я написал: этот исходный код был написан вслепую, когда я был дома на фестивале Qingming. То есть код не был терпеливо разобран, и кто-то попросил меня пойти выпить на Цинминский фестиваль, из-за чего я долгое время находился в состоянии рассеянности: «Я собираюсь пойти выпить после кодинга», так что некоторые форматы кодирования и дизайн структуры могут видеть случайные примеры повсюду~ В конце концов, это всего лишь демонстрация. В этом мире, если у вас есть природа Будды, просто следуйте судьбе! Если вы действительно хотите больше узнать о виртуальной машине Lua, то я рекомендую вам иметь время и терпение, чтобы прочитать исходный код виртуальной машины Lua~

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

Эта статья была разрешена автором для публикации сообщества Tencent Cloud +, студенты, которым нужен исходный код, нажмите:cloud.Tencent.com/developer/ ах…Скачать исходный код

вопросы и ответы

Что означает # в Lua?

Связанное Чтение

Профилирование производительности Lua

Используйте подсказки луа

Лучшие практики Openresty | Начало работы с Lua