TVM OpcodeTable c++

文章目录

  • [1 finalize()](#1 finalize())
  • [2 insert](#2 insert)
  • [3 lookup_instr](#3 lookup_instr)
  • [4 preload_at_least](#4 preload_at_least)

1 finalize()

c 复制代码
DispatchTable* OpcodeTable::finalize() {
  if (final) {
    return this;
  }
  instruction_list.clear();
  instruction_list.reserve(instructions.size() * 2 + 1);

  unsigned upto = 0;
  for (const auto& x : instructions) {
    auto range = x.second->get_opcode_range();
    assert(range.first == x.first);
    assert(range.first < range.second);
    assert(range.first >= upto);
    assert(range.second <= top_opcode);
    if (range.first > upto) {
      instruction_list.emplace_back(upto, new OpcodeInstrDummy{upto, range.first});
    }
    instruction_list.emplace_back(x);
    upto = range.second;
  }

  if (upto < top_opcode) {
    instruction_list.emplace_back(upto, new OpcodeInstrDummy{upto, top_opcode});
  }

  instruction_list.shrink_to_fit();
  final = true;
  return this;
}

这段代码是 OpcodeTable 类中的 finalize 方法的实现,它的作用是完成指令表的构建过程,确保所有的操作码都被正确地映射到对应的指令上,并且没有任何间隙。以下是该方法的详细步骤和逻辑:

  1. 检查是否已最终确定

    • 如果 final 标志已经设置为 true,表示指令表已经完成最终确定,直接返回 this 指针。
  2. 清空并预留空间

    • 清空 instruction_list,这是一个存储操作码和指令映射的列表。
    • 预留足够的空间以减少后续的内存重新分配。预留的空间是 instructions 映射大小的两倍再加1。
  3. 遍历指令映射

    • 遍历 instructions 映射,这个映射存储了操作码和对应的指令对象。
    • 对于每个条目,获取指令对象的操作码范围。
  4. 断言检查

    • 确保操作码范围的起始值等于映射中的键。
    • 确保操作码范围是有效的(起始值小于结束值)。
    • 确保操作码范围是连续的,没有间隙。
    • 确保操作码范围不超过最大操作码值。
  5. 处理间隙

    • 如果当前操作码范围的起始值大于 upto,表示存在间隙,需要插入一个 OpcodeInstrDummy 对象来表示这个间隙。
  6. 添加指令到列表

    • 将当前的操作码和指令对象添加到 instruction_list 中。
  7. 更新 upto

    • 更新 upto 为当前操作码范围的结束值。
  8. 处理剩余间隙

    • 如果 upto 小于最大操作码值,表示最后还有一段间隙,需要插入一个 OpcodeInstrDummy 对象来表示这个间隙。
  9. 优化内存使用

    • 调用 shrink_to_fit 方法来减少 instruction_list 占用的内存。
  10. 标记为最终确定

    • 设置 final 标志为 true,表示指令表已经完成最终确定。

finalize 方法是构建指令表的关键步骤,它确保了指令表的完整性和连续性。通过这个方法,可以确保:

  • 所有的操作码都被正确地映射到对应的指令上。
  • 没有任何操作码间隙,如果有,使用 OpcodeInstrDummy 对象来填充。
  • 指令表一旦最终确定,就不能再添加新的指令,这有助于提高指令查找的效率。

这个方法是虚拟机启动前必须执行的步骤,确保虚拟机能够正确地执行指令。

2 insert

c 复制代码
OpcodeTable& OpcodeTable::insert(const OpcodeInstr* instr) {
  LOG_IF(FATAL, !insert_bool(instr)) << td::format::lambda([&](auto& sb) {
    sb << "cannot insert instruction into table " << name << ": ";
    if (!instr) {
      sb << "instruction is null";
    } else if (final) {
      sb << "instruction table already finalized";
    } else {
      auto range = instr->get_opcode_range();
      sb << "opcode range " << td::format::as_hex(range.first) << ".." << td::format::as_hex(range.second - 1)
         << " already occupied or invalid";
    }
  });
  return *this;
}

这段代码是 OpcodeTable 类中的 insert 方法的实现,它用于将一个新的指令(OpcodeInstr 类型)插入到指令表中。如果插入失败,该方法会记录一个致命错误(FATAL 级别的日志),并提供详细的错误信息。以下是该方法的详细步骤和逻辑:

  1. 插入指令并检查结果

    • 调用 insert_bool 方法尝试将指令 instr 插入到指令表中,并检查返回值。
    • insert_bool 返回 true 表示插入成功,返回 false 表示插入失败。
  2. 记录错误信息

    • 如果 insert_bool 返回 false,使用 LOG_IF 宏记录错误信息。
    • LOG_IF 宏会在条件为 false 时执行日志记录,这里的条件是 !insert_bool(instr),即插入失败时。
  3. 格式化错误信息

    • 使用 td::format::lambda 和 lambda 表达式来格式化错误信息。
    • sb 是一个字符串构建器,用于构建错误信息。
  4. 检查指令是否为 null

    • 如果 instrnull,记录"instruction is null"。
  5. 检查指令表是否已最终确定

    • 如果指令表已经最终确定(final 标志为 true),记录"instruction table already finalized"。
  6. 检查操作码范围是否有效或已占用

    • 如果以上两个条件都不满足,获取指令的操作码范围,并检查是否有效或已占用。
    • 将操作码范围格式化为十六进制字符串,并记录错误信息。

insert 方法是指令表管理的关键部分,它确保了指令能够被正确地插入到指令表中。以下是该方法的一些重要作用:

  • 确保指令表的完整性:通过检查操作码范围的有效性和是否已占用,确保指令表的完整性和一致性。
  • 提供详细的错误信息:如果插入失败,提供详细的错误信息,包括指令表名称、失败原因等,有助于调试和问题排查。
  • 防止指令表被错误地修改 :通过检查 final 标志,防止在指令表最终确定后插入新的指令,确保指令表的稳定性。
  • 记录致命错误 :使用 FATAL 级别的日志记录错误信息,表明这是一个严重的问题,需要立即解决。

通过这个方法,可以确保指令表的正确性和稳定性,为虚拟机的正确执行提供基础。


3 lookup_instr

c 复制代码
const OpcodeInstr* OpcodeTable::lookup_instr(const CellSlice& cs, unsigned& opcode, unsigned& bits) const {
  bits = max_opcode_bits;
  unsigned long long prefetch = cs.prefetch_ulong_top(bits);
  opcode = (unsigned)(prefetch >> (64 - max_opcode_bits));
  opcode &= (static_cast<int32_t>(static_cast<td::uint32>(-1) << max_opcode_bits) >> bits);
  return lookup_instr(opcode, bits);
}

这段代码是 OpcodeTable 类中的 lookup_instr 方法的实现,它用于从 CellSlice 对象中提取操作码和位数,并查找对应的指令。以下是该方法的详细步骤和逻辑:

  1. 设置位数

    • bits = max_opcode_bits; 这行代码将 bits 设置为操作码的最大位数,这是预设的操作码长度。
  2. 预读取数据

    • unsigned long long prefetch = cs.prefetch_ulong_top(bits); 这行代码从 CellSlice 对象 cs 中预读取最多 bits 位的数据到 prefetch 变量中。prefetch_ulong_top 方法会读取最高位的 bits 位数据,形成一个无符号长整型数。
  3. 提取操作码

    • opcode = (unsigned)(prefetch >> (64 - max_opcode_bits)); 这行代码将预读取的数据右移 64 - max_opcode_bits 位,以将操作码移到最低位,然后转换为 unsigned 类型,得到操作码的值。
    • opcode &= (static_cast<int32_t>(static_cast<td::uint32>(-1) << max_opcode_bits) >> bits); 这行代码通过位与操作来清除 opcode 中超出 max_opcode_bits 位数的高位。这里使用了 -1 左移 max_opcode_bits 位来创建一个掩码,然后右移 bits 位来适配实际的操作码位数。
  4. 查找指令

    • return lookup_instr(opcode, bits); 最后,使用提取的操作码和位数调用另一个 lookup_instr 方法来查找对应的指令,并返回找到的指令对象的指针。

这个方法的作用是从 CellSlice 对象中提取操作码和位数,并使用这些信息来查找对应的指令。这是虚拟机执行流程中的关键步骤,因为它允许虚拟机根据指令流中的操作码来确定要执行的具体指令。

  • 预读取和提取操作码:这个方法通过预读取和位操作来提取操作码,这是处理指令流的基础。
  • 位数适配:通过位与操作来确保操作码只包含有效的位数,这对于处理不同长度的操作码非常重要。
  • 指令查找:使用提取的操作码和位数来查找指令,这是虚拟机执行指令前的必要步骤。

这个方法的设计使得虚拟机能够灵活地处理不同长度的操作码,并且能够从数据流中准确地提取出操作码信息,为执行指令做好准备。

这行代码中的 bits 变量代表了操作码的实际位数。在虚拟机的上下文中,操作码的位数通常是指操作码所需的位数,这个位数可能小于或等于操作码可能的最大位数(max_opcode_bits)。以下是代码中如何确定 bits 的步骤:

  1. 预设最大位数

    • bits = max_opcode_bits; 这行代码将 bits 初始化为操作码可能的最大位数,这是一个预设值,用于后续的位操作。
  2. 预读取数据

    • unsigned long long prefetch = cs.prefetch_ulong_top(bits); 这行代码从 CellSlice 对象 cs 中预读取最多 bits 位的数据。这里的 bits 还是预设的最大位数。
  3. 提取操作码

    • opcode = (unsigned)(prefetch >> (64 - max_opcode_bits)); 这行代码将预读取的数据右移,以便将操作码移到最低位。
  4. 位数适配

    • opcode &= (static_cast<int32_t>(static_cast<td::uint32>(-1) << max_opcode_bits) >> bits); 这行代码通过位与操作来确保 opcode 只包含有效的位数。

在位数适配这一步,bits 的值实际上是在之前的步骤中确定的,通常是通过分析指令集架构或运行时环境来确定的。例如,如果指令集架构规定了操作码的最大长度,那么 max_opcode_bits 就会被设置为这个值。在执行期间,虚拟机会根据实际遇到的操作码来确定 bits 的值。

在某些情况下,bits 可能是动态确定的,例如,如果操作码的长度可以根据上下文变化,那么 bits 可能会根据特定的指令或数据来调整。在这种情况下,bits 的值可能会在执行期间通过某种机制(如解析操作码前缀或根据操作码的模式)来确定。

总的来说,bits 的值是由虚拟机的设计和指令集架构决定的,它代表了操作码所需的位数,这个值在执行期间用于正确地提取和解释操作码。

4 preload_at_least

这段代码是 CellSlice 类中的 preload_at_least 方法的实现,它用于确保 CellSlice 对象中至少预加载了 req_bits 位数据。这个方法是私有的,可能是为了内部处理而设计的。以下是该方法的详细步骤和逻辑:

  1. 参数和状态检查

    • assert(req_bits <= 64 && have(req_bits) && ptr); 这行代码断言 req_bits 不超过 64,并且至少有 req_bits 位数据可用,同时 ptr 指针不为空。
  2. 检查是否已满足要求

    • if (req_bits <= zd) { return; } 如果已经预加载的位数 zd 大于或等于 req_bits,则不需要进一步预加载。
  3. 计算剩余位数

    • int remain = bits_en - bits_st - zd; 计算剩余的位数,即总位数减去起始位数和已预加载的位数。
  4. 处理32位数据

    • 如果 zd 小于或等于 32 并且剩余位数 remain 大于 24,那么从 ptr 指向的位置加载一个32位的数据,并将其左移至 z 的正确位置。
    • 更新 ptr 指针和 zd(已预加载的位数)。
  5. 处理剩余位数

    • 如果剩余位数 remain 小于或等于 32,那么更新 zd 并返回。
    • 否则,继续处理剩余的位数。
  6. 循环处理剩余位数

    • while (zd < req_bits && remain > 0) 循环直到 zd 达到 req_bits 或者没有剩余位数。
    • 如果 zd 大于 56,那么从 ptr 指向的位置加载数据,并将其右移至 z 的正确位置。
    • 否则,从 ptr 指向的位置加载数据,并将其左移至 z 的正确位置。
    • 更新 ptr 指针和 zd
  7. 更新剩余位数

    • 如果剩余位数 remain 小于或等于 8,那么更新 zd 并返回。
    • 否则,更新 zdremain

preload_at_least 方法的作用是确保 CellSlice 对象中至少预加载了 req_bits 位数据,这对于后续的位操作和数据提取非常重要。以下是该方法的一些重要作用:

  • 确保数据可用性:通过预加载足够的位数,确保后续操作有足够的数据可用。
  • 提高效率:通过预加载数据,减少后续操作中的内存访问次数,提高效率。
  • 处理不同长度的数据:能够处理不同长度的数据,包括32位和非32位的数据。

这个方法是 CellSlice 类中处理位数据流的关键部分,它为后续的位操作提供了基础。通过预加载足够的数据,可以确保后续操作的正确性和效率。

相关推荐
Moweiii27 分钟前
SDL3 GPU编程探索
c++·游戏引擎·图形渲染·sdl·vulkan
渝妳学C1 小时前
【C++】类和对象(下)
c++
EleganceJiaBao1 小时前
【C语言】结构体模块化编程
c语言·c++·模块化·static·结构体·struct·耦合
xianwu5431 小时前
反向代理模块。开发
linux·开发语言·网络·c++·git
Bucai_不才2 小时前
【C++】初识C++之C语言加入光荣的进化(上)
c语言·c++·面向对象
木向2 小时前
leetcode22:括号问题
开发语言·c++·leetcode
筑基.2 小时前
basic_ios及其衍生库(附 GCC libstdc++源代码)
开发语言·c++
yuyanjingtao2 小时前
CCF-GESP 等级考试 2023年12月认证C++三级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
王老师青少年编程4 小时前
gesp(二级)(12)洛谷:B3955:[GESP202403 二级] 小杨的日字矩阵
c++·算法·矩阵·gesp·csp·信奥赛
dingzd954 小时前
走进 Web3:探索分布式网络的未来
web3·去中心化·区块链·互联网