【外评】在 RiSC-V 上运行《巫师 3》游戏

距离我们上次更新 RISC-V 后端状态已经过去一年多了,最近我们成功地在 RISC-V PC 上运行了《巫师 3》,我相信这是有史以来第一款在 RISC-V 机器上运行的 AAA 级游戏。因此,我认为这是写一篇更新的绝佳时机,于是就有了这篇文章。

https://www.youtube.com/embed/5UMUEM0gd34  通过 Box64、Wine 和 DXVK 在 RISC-V 上运行《巫师 3》。

故事

一年前,RV64 DynaRec 只能运行一些相对 “易跑 ”的原生 Linux 游戏,如《星露谷物语》、《World of Goo》等。

一方面,这是因为大量新的 x86_64 指令在 RISC-V 中快速实现后,DynaRec 中留下了许多 bug。如果不正确实现 x86_64 ISA,事情就无法进行。但最重要的因素是,我们当时没有可以插入 AMD 显卡的 RISC-V 设备,VisionFive 2 和 LicheePi 4A 上的 IMG 集成显卡不支持 OpenGL,只支持 OpenGL ES。

我们可以使用 gl4es 获得一定程度的 OpenGL 支持,这样就能运行《星露谷》等游戏,但对于其他更严肃的 Linux 游戏以及所有 Windows 游戏来说,这还远远不够。

因此,这成为我们在更广阔的世界中测试更多 x86 程序的一道难以逾越的障碍,直到我和 ptitSeb 从 Sophgo 收到了 Milk-V Pioneer,这是一台 64 核 RISC-V PC,当然,它还有一个用于显卡的 PCIe 插槽。非常感谢 Sophgo!

此外,另一位核心贡献者 xctan 也找到了通过 M.2 接口将 AMD 显卡 “插入 ”VisionFive 2 的方法。就这样,我们接触到了更广阔的世界,并修复了大量 RV64 DynaRec Bug,还添加了大量新的 x86 指令。量变导致质变,越来越多的游戏开始正常运行,最后,我们第一次尝试运行《巫师3》,它就这样正常运行了!

这就是在 RISC-V 上运行《巫师 3》的故事。

RISC-V DynaRec 的现状如何?

x86 指令集非常非常大。据粗略统计,ARM64 后端总共实现了 1600 多条 x86 指令,而 RV64 后端实现了约 1000 条指令。其中,有 300 多条指令是新支持的 AVX 指令,而我们在 RISC-V 中根本没有实现这些指令。总之,我们还需要继续努力。

另外,对于 SSE 指令,我们使用标量指令来实现,而 AArch64 使用 Neon 扩展,LoongArch64 使用 LSX 扩展。因此,与其他两个后端相比,性能相当差。

不过,事情并非一成不变。RISC-V 有一个名为 “向量扩展 ”的向量扩展。是的,我知道,所以从现在起我就叫它 RVV。

目前市场上已经有一些支持 RVV 的设备,例如上文提到的 Milk-V Pioneer,它支持 RVV 0.7.1 版本的一个变体 xtheadvector 扩展(事情有点复杂)。此外,不久前发布的 SpacemiT K1/M1 SoC 也支持 RVV 1.0 批准版本。目前,配备该 SoC 的 Banana Pi F3 和 Milk-V Jupiter 已经上市。

有了这些设备,我们最近为 box64 添加了基本的 RVV 支持,并实现了几种常见的 SSE 指令。不过,这项工作仍处于初期阶段,因此目前对性能没有帮助。但未来还是充满希望的,不是吗?

接下来,让我们谈谈笼罩在 RISC-V 后端上空的两朵乌云。在过去的一年里,我认为 RISC-V 在 x86 仿真中最欠缺的就是这两个方面。

x86 仿真最需要的指令

至少在 x86 仿真方面,在我们支持的所有 3 种架构中,RISC-V 是表现力最差的一种。与 AArch64 和 LoongArch64 相比,RISC-V 缺乏许多方便的指令,这意味着我们必须使用更多的指令来仿真相同的行为,因此翻译效率会更低。

其中,有两条指令最为关键–从一个寄存器中提取一定范围的位到另一个寄存器中;从一个寄存器中插入一些位到另一个寄存器的范围中。

LoongArch64 和 AArch64 都有等效的指令,但 RISC-V 世界中却没有与这两条指令对应的指令,无论是官方指令还是厂商扩展指令。这并不是什么破坏 RISC 理念的复杂指令,因此它们不存在于 RISC-V 上实在令人遗憾。

但为什么它对 x86 仿真如此重要呢?因为 x86 ISA 倾向于保留不变位。

例如,对于 ADD AH, BL 指令,box64 需要从 RBX 中提取最低字节,添加到 RAX 的第二个最低字节,然后将其插入 RAX 的第二个最低字节,同时保持 RAX 中的所有其他字节不变。

在 LoongArch64 上,我们有 BSTRPICK.D 来提取比特,BSTRINS.D 来插入比特,因此执行过程如下:

BSTRPICK.D scratch1, xRAX, 15, 8
BSTRPICK.D scratch2, xRBX, 7, 0
ADD scratch1, scratch1, scratch2
BSTRINS.D xRAX, scratch1, 15, 8

简单直观,对吧?在 ARM64 上,使用 UBFX 和 BFI 操作码也同样简单。但在 RISC-V 上,我们必须这样做:

# extract the second lowest byte of RAX
SRLI scratch1, xRAX, 8
ANDI scratch1, scratch1, 0xFF
# extract the lowest byte of RBX
ANDI scratch2, xRBX, 0xFF
# do the addition
ADD scratch1, scratch1, scratch2
# fill scratch3 with mask 0xFFFF_FFFF_FFFF_00FF
LUI	scratch3, 0xFFFF0
ADDIW   scratch3, scratch3, 0xFF
# insert it back
AND xRAX, xRAX, scratch3
ANDI scratch1, scratch1, 0xFF
SLLI scratch1, scratch1, 8
OR xRAX, xRAX, scratch1

因此,一个简单的字节相加就需要整整 10 条指令,而这绝非个例!x86 中有许多类似的指令,而它们在 RISC-V 上的实现更为繁琐。

16 字节原子指令的挫败感

x86 有用于无锁原子操作的 LOCK 前缀指令,而 box64 主要使用 LR/SC 序列来模拟这些指令。LR/SC 是 Load-Reserved / Store-Conditionally 的缩写。

例如,对于 LOCK ADD [RAX], RCX,我们生成以下代码:

MARKLOCK:
LR.D scratch1, (xRAX)
ADD scratch2, scratch1, xRCX
SC.D scratch3, scratch2, (xRAX)
BNEZ scratch3, MARKLOCK

如果 RAX 中的地址是非对齐的,情况就会变得复杂一些,但总的来说,这样做的效果非常好。

LOCK CMPXCHG16B 指令除外,该指令将 RDX:RAX 与 16 字节内存进行比较,并将 RCX:RBX 与内存地址进行交换。虽然可以使用 AArch64 和 LoongArch64 中的一些 16 字节原子指令来实现这一功能,但遗憾的是,RISC-V 中没有与之对应的指令。

因此,我们无法像其他架构那样完美地实现这条指令,更不幸的是,许多程序都使用了这条指令,例如 Unity 游戏。

结束

最后,尽管有这么多不足之处,《巫师3》还是在游戏中以高达15帧/秒的速度运行,并在主菜单上以box64全速运行!对于一台从未设计用于运行 AAA 级游戏的机器来说,这还不算太差!

本文文字及图片出自 What It Takes to Run The Witcher 3 on RiSC-V

你也许感兴趣的:

共有 141 条讨论

  1. 我想请教一个不懂芯片的人一个问题:软件工程师在为 RISC5 开发软件时,需要做哪些不同的工作?

    我认为可执行文件的大小会增加,这意味着必须针对高速缓存的局部性进行积极优化?

    我认为某些类型的软件更适合 CISC 或 RISC,如游戏、网络服务器?

    1. 使用压缩指令扩展的 RISC-V 平均体积实际上比 x86-64 和 ARM 更小。

      在软件方法上需要改变的内在因素并不多。与 x86-64 相比,RISC-V 最大的优势可能是有 32 个寄存器(x86-64 为 16 个),可以在数据开始溢出堆栈之前获得更多的中间值,这同样适用于 ARM(也有 32 个寄存器)。但一般来说,除非进行微优化,否则这并不重要。

      更多的微优化可能包括

      – 矢量扩展(又称 V 或 RVV)并不在 rv64gc ISA 的基础中,因此您可能无法获得 SIMD 优化,这取决于目标;而 x86-64 和 aarch64 的基础中包含 SSE2 和 NEON(128 位 SIMD)。

      – 同样,基本 rv64gc(需要 Zbb)中没有 popcount 和前导零/尾数零计数;基本 x86-64 没有 popcount,但有 clz/ctz。

      – 效率较低的无分支选择,即 “a ? b : c”;在基本 rv64gc 上需要 ~4-5 个实例,使用 Zicond 时需要 3 个,但在 x86-64 和 aarch64 上只需要 1 个。有些硬件还能将跳转融合到 mv 指令上,从而有效地实现无分支,但这更需要针对具体目标。

      RISC-V 配置文件解决了前两个问题(例如,Android 需要 rva23,而 rva23 需要 rvv、Zbb 和 Zicond 等),但如果 linux 发行版决定使用 rva20/rv64gc,那么他们将永远无法在未使用动态分派的预编译代码中使用这些扩展。虽然这也是 x86-64 的问题(ARM 的问题要小得多,因为它没有那么多扩展;SVE 可能是迄今为止最大的问题,但仍未得到广泛支持(即苹果芯片不支持))。

      1. 这似乎是编译器通常会处理的问题,不是吗?显然,这并不适用于所有地方,但在一般情况下应该是这样。

        1. 矢量的东西一般都是用本征函数或汇编手工编码的。自动矢量化的结果好坏参半,因为没有办法要求编译器保证已对代码进行了矢量化。

          但对于这样的仿真器,box64 必须选择如何在 RiscV 上仿真矢量化指令(例如缓慢使用标量或尝试使用本地矢量指令重新实现)。当然,面临的挑战是,除非仿真器能即时重写代码,否则性能通常不会太好,因为 1:1 映射与注意到正在执行的高级操作模式,并提供更优化的实现,一次性替换另一大块指令,以考虑芯片上的实现差异(例如,您可能必须仿真丢失的指令,但如果有完成相同高级计算的替代方法,重写器可以跳过仿真)相比,1:1 映射将是次优的。

          当然,从性能的角度来看,这样做的最大挑战在于如何高效地翻译 GPU 的内容,以便使用本地驱动程序代码,而 Riscv 很可能依赖于开放源码软件的 GPU 驱动程序(如果游戏仅限于 Windows 操作系统,则可能需要使用葡萄酒来添加另一个翻译层)。

          1. 我认为它使用的是 RADV,与 Steam Deck 相同。对于大多数工作负载而言,这比 AMD 自带的驱动程序更快。没错,它使用的是 Wine 和 DXVK。就游戏而言,它运行在支持 DirectX 的 x86 Windows 机器上。翻译层可真不少。

          2. 在 clang 中,您实际上可以使用 “#pragma clang loop vectorize(enable) ”来要求它对特定循环的矢量化错误发出警告:https://godbolt.org/z/sP7drPqMT(您甚至可以将其设为错误)。

            甚至还可以使用 “#pragma clang loop vectorize(assume_safety) ”来告诉它指针别名不会成为问题(gcc 也有类似的 “#pragma GCC ivdep”),这应该能消除大多数矢量化遗漏的奇怪原因。

          3. > 矢量程序通常使用本征函数或汇编语言手工编码。自动矢量化的结果好坏参半,因为没有办法要求编译器保证已对代码进行了矢量化。

            没错,但大多数情况下,这些都是针对特定体系结构的,而 RVV 1.0 与 NEON 或 SSE2 有很大不同,因此无论如何都需要修改。此外,这些寄存器通常使用专用寄存器,而不是通用寄存器。我并不是说没有工作可做(尤其是像这种对性能极为敏感的应用程序),我只是说大多数应用程序不会有这些问题,也不会对寄存器溢出如此敏感。

            1. 我想强调的是,编译器并不能像处理寄存器分配和指令选择那样自动地处理矢量代码,而寄存器分配和指令选择是更容易解决的问题。而且很容易想象,编译器无法在架构相当新颖的系统上优化代码。实际上,RISCV 和 ARM 在高层架构上并无太大差异,因此不需要编写完全不同的优化代码,甚至可以根据架构选择性地加权,但我认为,像 Mill CPU 这样的产品可能需要重新构思,才能获得接近最佳的性能。

          4. 我在某处读到过,由于浮点加法不是关联加法,编译器不会自动矢量化,因为顺序可能会改变。

            1. 比这要复杂一些(假设您的热路径是浮点加法而不是积分加法),但这可能是一个考虑因素。

              1. 还有哪些其他考虑因素?(假设我们处理的是 FP)

                1. 免责声明:我不是这方面的专家,所以可能错得离谱。这只是我的理解,所以很高兴得到纠正。

                  另一个问题是,像融合多重加法这样的运算会有不同的精度(如果我没记错的话,会更高),这违反了 IEE754,因此也违反了矢量化,因为默认选项是符合标准的。

                  另一个原因是,某些数学固有函数在文档中会填充 Errno,这将阻止在具有固有函数的路径中使用 autovec。

                  浮点数与双倍数之间可能还有其他细微差别。

                  基本上,我认为构成 ffast-math 的大多数东西都会阻止自动矢量化。

                  1. 融合乘法和加法同样适用于标量代码和矢量化代码(C 语言实际上允许编译器融合它们;有 -ffp-contract=off / FP_CONTRACT pragma 可以关闭);如果编译器/自动矢量化程序有此要求,完全可以将乘法和加法分开(可能比融合它们更慢?但鉴于两者具有相同的 fma 适用性,因此对标量与矢量完全没有影响)。

                    对于 <math.h> Errno,有 -fno-math-errno;事实上,它包含在 -ffast-math中,但你并不需要它的全部功能。

                    我认为,带有浮点累加器的循环是唯一一种需要使用 -ffast-math 来实现自动矢量化的情况(即便如此,我想也有一些子标志可以让你在获得假定关联性优化的同时,仍然允许 NaN/inf)。

        2. 这是编译器会处理的问题,但仍会适度影响编程决策,即在由于溢出存储/加载而导致速度开始变慢之前,你可以拥有更多的临时变量(尤其是在有函数调用的循环中,因为更多的寄存器也意味着更多的非易失性寄存器(即那些保证不会在函数调用中发生变化的寄存器))。不过,即便如此,影响也非常有限。

          1. 在制作(语言)运行时,我肯定会考虑到这一点,但除了对性能最敏感的应用程序外,其他应用程序可能根本不会考虑到这一点。当然有区别,但比大多数应用程序要求的级别要低得多

            1. 是的。不幸的是,我是一个要制作语言运行时的人:)

              这只是我一开始能想到的最重要的潜在因素。也许 RVV 不在 rva20/rv64gc 中更有意义。

              1. 看起来像是 APL 项目?真是酷毙了!

    2. > 向不从事芯片工作的人提个问题:软件工程师在为 RISC5 开发软件时,需要做哪些不同的工作?

      大多数情况下,没有什么不同;用 C 语言等高级语言编写的代码应该可以正常工作。最大的区别是内存模型较弱,这在大多数非 x86 架构(如 ARM)上也存在(而且您的代码首先不应该依赖于强大的内存模型)。

      > 我认为可执行文件的大小会增加,这意味着必须对缓存位置进行积极优化?

      由于历史原因,x86 上的可执行代码密度并不高,因此可执行文件的大小不会像你想象的那样增加;RISC-V 及其压缩指令扩展和 32 位 ARM 及其 Thumb 扩展都相当紧凑(如果你想了解更多信息,有一篇早期的 RISC-V 论文对代码大小进行了比较)。

      > 我认为某些类型的软件更适合 CISC 或 RISC,如游戏、网络服务器?

      最重要的不是 CISC 与 RISC,而是矢量指令和密码学扩展的存在和质量。有些软件,如视频编码和解码,主要依靠矢量指令来获得良好的性能,而像全盘加密或散列,则可以通过专门的指令来加速特定的算法,如 AES 和 SHA256。

    3. 不,任何 ISA 对于任何类型的工作负载都应该同样出色。如果你正在进行汇编编程,那么它就会有所区别,但如果你正在用 Python 或 Unity 做一些事情,那就真的不重要了。

      这更多的是为了摆脱 ARM 的专利束缚,利用所学到的经验重新开始。

  2. 这让我想起一个著名的俄罗斯人是如何在 Elbrus 8S 上运行 Atomic Heart 的。

    不过 Elbrus 有原生翻译器,而且相当不错。原子之心》还算能玩,每秒 15-25 帧。

    1. 这家伙:https://www.youtube.com/watch?v=-0t-5NWk_1o

  3. 文章对 “基础知识 ”的介绍有点少–我以为他们使用了某种葡萄酒移植程序来运行它。不过,他们似乎以某种方式在 RISC-V 芯片上实现了 x86_64 ISA – 谁能详细介绍一下这部分是如何实现的?

    1. 基本信息在这里: https://box86.org/ 它是一个模拟器,但是:

      > 因为 box86 使用了一些 “系统 ”库的本地版本,如 libc、libm、SDL 和 OpenGL,所以很容易与大多数应用程序集成和使用,而且在某些情况下性能会出奇的高。

      Wine 也可以作为本地程序编译/运行。

      1. > Wine 也可以编译为本地程序并运行。

        我不确定能否在 RISC-V 上以本地方式运行 Wine 来运行 x86 Windows 程序,因为 Wine 不是模拟器。Wine 有一个 ARM 端口,但只能运行 Windows ARM 程序,不能运行 x86 程序。

        相反,box64 运行的是 x86_64 Wine https://github.com/ptitSeb/box64/blob/main/docs/X64WINE.md。

        1. 理论上应该可以构建 Wine,以便在编译为 ARM/RISCV 的同时提供 x86_64 API。您的链接没有明确说明是否正在这样做。

          (尽管我认为,在提供一种架构的应用程序接口的同时为另一种架构构建应用程序接口,说起来容易做起来难。首先,工具链对这种诡计往往不合作)。

          1. Box64 的文档只介绍了如何从 winehq 软件仓库安装 Wine x64 版本,因为大多数 arm 软件仓库并不支持 x64 软件。甚至可以用运行 Windows 游戏的 x64 Proton 来运行 Steam。至少在 ARM 上是这样,RISC-V 就不一定了。

            Wine 自己的文档说它需要一个模拟器: https://wiki.winehq.org/Emulation

            > 由于 Wine 并非模拟器,因此仅靠 Wine 无法在其他架构上运行所有这些应用程序。

            你的意思是将 x86_64 Windows API 作为本地 RISC-V/ARM API 提供给模拟器层?这需要对模拟器进行更深入的整合,但 Box64/box86 已经对一些 Linux 库做了这样的工作:拦截 api 调用并用本地库替换它们。不确定是否适用于 wine

            1. > 但这正是 Box64/box86 对某些 Linux 库所做的:拦截 api 调用并用本地库替换。不确定是否适用于 wine

              是的,我就是这个意思。毕竟原理很简单:将 AMD64 调用转化为 ARM/RISCV 调用,然后将其传递给本地代码。

              对 Wine 来说,要做到这一点非常棘手(需要覆盖的面积更大,某些特定于 Win32 Arch 的结构之间可能存在差异等等),所以我敢打赌,开箱即用并非如此,但我无法通过浏览 box64 repo 来确定这一点。

              1. 正如微软自己在 Windows 11 中演示的那样:https://learn.microsoft.com/en-us/windows/arm/arm64ec

  4. 令人难以置信的结果!这是一项艰巨的工作,在某些情况下 RV 似乎已达到极限。位收集和分散指令应该成为一种扩展!

    1. 如果能看到一款更依赖图形核心而非 CPU 的游戏的测试结果,将会非常有用。也许是《神界 2》?

  5. > 至少在 x86 仿真方面,在我们支持的所有 3 种架构中,RISC-V 是表现力最差的一种。

    在计算机科学历史课上,RISC 曾被解释为精简指令集计算机,但我看到很多文章和拟议中的新 RISC-V 配置文件都在说 “我们只需要增加几条指令就能实现功能平等”。

    我明白 RISC-V 对大多数人来说只是其他平台的一种便捷替代方案,但这是否也意味着 RISC 梦想已死?

    1. 正如我所听到的解释,RISC 在实践中并不强调 “绝对简约的指令集”,而更强调 “不要添加任何方便汇编程序员的功能或其他类似的小聪明,尽可能依赖编译器而不是前端芯片”。

      虽然我记得在阅读 RISC-V 规范时,RISC-V 对于在前端可以融合通用指令序列的情况下不添加 “组合 ”指令有相当严格的要求。

      对于 RISC-V 相对于 x86/ARM 的缺点,我(远非专家)的印象更多的是,规范是从最基本的嵌入式芯片开始编写的,然后随着时间的推移增加了更多的应用处理器扩展。(不幸的是,他们花了很长时间才完成位操作和 simd/vector 扩展的工作,这就造成了我们现在所说的功能差距。

      因此,我不认为这些差距是 RISC 原教旨主义造成的,根本不存在这样的问题。

      1. 换一种说法,“尽量避免那些无法在一个时钟周期内执行的指令,因为这些指令会引入硅复杂性”。

        1. 但事实也并非如此,例如任何除法或内存操作。

          实际上,“RISC ”或 “CISC ”已经不存在了,它们几乎都趋同了。充其量你可以说 “RISC ”现在只是意味着没有任何混合加载 + alu 指令,但这些指令在 x86 中也用得不多。

          1. 你一针见血。实际上,当人们抱怨 CISC 与 RISC 时,他们主要是在抱怨两件事。首先是 x86 处理器背上了传统的包袱(也就是它们有很长的成功历史,一直延续到今天),其次是 x86 有很多可变长度的指令。除此之外,大多数抱怨都是吹毛求疵,比如通用寄存器的数量和命名方式。

      2. >以及更多关于 “不要添加任何汇编程序方便或其他类似的小聪明,尽可能依靠编译器而不是前端芯片 ”的内容

        这样做有什么好处?

        1. 它将实现的复杂性从硬件转移到了软件上。这并不是固有的优势,但多一个编译器通道通常比增加硅芯片面积要便宜。

          另外,从安全角度来看,如果你的芯片 “太聪明”,引入了安全漏洞,那你就完蛋了。另一方面,软件可以打补丁。

          1. 老实说,我觉得编译器/解释器复杂性的缺乏令人沮丧。

            我经常觉得,作为一个社区,我们并没有兴趣制作比我们最初使用的工具更好的工具。

            与编译器交流,用代码生成代码,从编译器获取反馈信息,这些都应该是标准的事情。一般来说,它们不应该被使用,但如果我们也能在我们的服务中更好地使用剖析,我们就可以让团队中的专家使用特殊工具来改进关键部分。

            我知道我们中的许多人在项目中的构建时间已经很短了,但我觉得这是拒绝以类似方式改进 ci/cd/构建工具的副作用。

            如果你曾经开发过现代 TypeScript 框架应用程序,你就会明白我的意思。你可以创建装饰器和宏,与 TypeScript 编译器对话,要求它生成一些额外的 JS 或修改它生成的 JS。整个框架就坐在那里为你运行部分重构并刷新浏览器。

            这让像 golang 这样的东西感觉像是 80 年代的产品。

            该死的 golang… 我明白,宏、装饰器和泛型被过度使用了。但我要做一个库,让我公司的 2100 名开发人员都能标准化…… 我需要一些元编程工具。

            1. 我通常会谈论很多 Oberon 或 Limbo,但它们的设计受限于 20 世纪 90 年代的硬件成本,以及替代品所需的更多资源。

              我们距离那个时代已经有三十年的时间了,运行那些系统的硬件绰绰有余,但只有大学或财力雄厚的公司才有。

              然而,Go 文化并没有更新,或者说更新得很不情愿,犯了在 1.0 版本问世时就已经出现的错误。

              自从他们在 CNCF 项目上一炮而红后,一些工作几乎无法避免。

        2. 编译器移除的复杂性不必在运行时由 CPU 处理

          1. 当然,但这并不一定与 “程序员的便利或其他类似的聪明才智 ”相矛盾,不是吗?

            1. Riscv的观点是,汇编程序员接口应尽可能通过伪指令来处理,这些伪指令在转为机器码时就会消失,而不是让芯片来处理它们。

        3. 指令可以在一个时钟周期内完成,与需要多个时钟周期的指令相比,这消除了很多复杂性。

          复杂性的降低意味着你可以在相同数量的硅片上装入更多的东西,并以更低的功耗实现更快的速度。

          1. 事实并非如此;许多 RISC 风格的指令需要多个(有时是多个)时钟周期才能完成,例如 mul/div、浮点运算和分支指令通常也需要一个时钟周期以上,再加上流水线、高速缓存、MMU、原子化…… “一个时钟周期 “其实并不意味着什么。尤其是更先进的 CPU 在理想情况下每个时钟周期可以执行多条指令。

            当然,加法和在寄存器之间移动位需要一个时钟周期,但这些指令在 CISC 上也需要一个时钟周期。如果你真的吝惜硅片,非常小的 RISC 微控制器的加法和移位操作可能需要一个以上的周期。

            (内存操作当然也需要多个周期,但这不是 CPU 的错)。

            1. >很多 RISC 风格的指令需要多个(有时是多个)时钟周期才能完成,如 mul/div、浮点运算等。

              这似乎是你想要支持的东西,但这似乎是在反对?

            2. 明白了,所以更多的是要删除微代码。

              1. 最大的区别在于,RISC 指令只能出现一次异常,而 x86 rep mov 等指令却可以出现不计其数的页面错误。

                1. 事实并非如此,因为同一条指令可能会出现大量异常。例如,一条加载指令可能会出现所有这些异常,甚至更多(但一次只会报告一个):指令取回页面故障、加载错位和加载页面故障。

                  更有特点的是关于副作用的假设(整数指令无副作用,FP 指令有累积标志)和所需寄存器文件端口的数量。

    2. 为了让学生在一个学期的课程中就能实现指令集,你需要进行简化,比如让所有指令都有两个输入和一个输出。这也让研究人员在进行处理器设计实验时变得更加简单。但这也意味着,要想获得更高的性能,一些方便的指令就不能使用了。

      这还不是全部,对于追求高性能设计的团队来说,更简单的流水线需要更少的工程资源,因此他们可以花更多的时间进行优化。

      RISC 通常是一种简化理念,但也可以进一步简化。MIPS 的简化程度几乎与 RISC-V 不相上下,但 ARM 和 POWER 的简化程度更为适中,在高性能领域与 x86 相比似乎毫不逊色。

      但请记住,除了运行应用程序之外,处理器还有许多其他用途。嵌入式、加速器等。在特定的应用内核领域,我对 RISC-V 有些悲观,但从更广泛的角度来看,我认为它有很大的潜力,可能至少会在几个商业领域占据主导地位,同时也是一个很好的教学和研究工具。

    3. RISC 的梦想是简化 CPU 设计,因为大多数软件都是使用编译器而不是直接汇编编写的。

      经典 RISC 的特点:

      – 大多数数据操作指令只对寄存器有效。

      – 内存指令一般只加载/存储到寄存器。

      – 这意味着你需要很多寄存器。

      – 自己做堆栈,因为无论如何你都必须手动操作堆栈来传递参数。因此不需要 CALL/JSR 指令。使用一些直接加载/存储到指令指针寄存器的基本指令,自己实现堆栈。

      – 指令编码是可预测的,而且每条指令的大小相同。

      – 不止一个 RISC 架构有一个寄存器,读数总是 0,不能写入。该寄存器用于将内容设置为 0。

      这个寄存器很有用,但随后出现的问题让它变得不那么重要了:

      – 非顺序执行–一般来说,原始指令流声明了通向所需结果的路径,但并不一定是 CPU 真正在做的事情。推测执行、分支预测和寄存器重命名等都是造成这种情况的原因。

      – SIMD – 基本上是一个独立的宽寄存器空间,其指令适用于这些宽寄存器内的所有值。

      因此,真正的 OOO 和 SIMD 应运而生。

    4. 有 RISC 梦吗?我认为有效率 “梦想”,有性能 “梦想”,有成本 “梦想”–甚至有相对于成本、性能和效率的低复杂性 “梦想”–但 RISC 梦想呢?比起成本、性能、效率和简单性,谁更关心 RISC?

      1. 有这样一个梦想。它是要让 CPU 变得简单得让人匪夷所思,把高速缓存放到过去所有控制逻辑所在的空地上,然后把时钟调到最高,让软件来处理加载/分支延迟、有效使用所有 64 个寄存器等问题。这样就能在性能上击败那些愚蠢的 CISC 架构,而且设计和生产成本也只需它们的一小部分!

        这并不奏效,主要有两个原因:首先,仅仅把时钟调到很高并不足以获得出色的性能:如果你需要惊人的性能,你确实希望你的 CPU 是超标量、无序的,并且具有出色的分支预测器。但当你做到这一切时,RISC 解码的简单性就不再那么重要了,正如奔腾 II 在性能上与 DEC Alpha 不相上下时所证明的那样,同时它还具有字节加载/存储等实用功能。是的,在引擎盖下是类似 RISC 的指令,但这只是实现细节,没有理由在 ISA 中向用户公开,就像你没有必要在 ISA 中公开分支延迟槽一样,因为这样做是个坏主意:例如,MIPS II 增加了一个额外的流水线级,现在他们需要两个分支/加载延迟槽。哎呀!所以他们还是增加了联锁(MIPS 最初的意思是 “没有联锁流水线级的微处理器”,哈哈),并去掉了负载延迟;由于向后兼容性的原因,他们还是留下了一个分支延迟槽,而所需的电路可以说是愚蠢的。

        第二个原因是,软件(更准确地说,是编译器)无法很好地处理第一段中的所有问题。这就是 Itanium 失败的原因。这就是为什么没有人再生产带有寄存器窗口的 CPU 了。编译器中的静态指令调度仍然无法击败动态指令重新排序。

        1. 这篇文章写得很好,因为它也可以直接用来推翻 “arm 指令集让整个 CPU 比 x86 芯片更好 ”的说法。这可能是真的,而且占总优势的 0.1%(估计值);实际上,引擎盖下全部都是 RISC,两种 ISA 都需要解码器,x86 可能需要一个稍大的解码器,这在面积上相当于会计噪声。

          例如,https://chipsandcheese.com/2021/07/13/arm-or-x86-isa-doesnt-…

        2. > 这并不可行

          ……但它确实成功了。

          你让学生设计的芯片性能超过了需要庞大团队和巨额投资的工业核心。

          橡果公司(Acorn)的一个只有几个人的团队,用可能只有 i460 1/100 的投资,制造出了性能超过 i460 的内核。更不用说更加昂贵的 VAX 芯片了。

          你能想象当时的 DEC 工程师们是多么困惑,因为他们复杂得离谱、昂贵得离谱的 VAX 芯片被一群初次接触芯片设计的人打得落花流水?

          > 奔腾 II

          该芯片于 1997 年问世。最初的 RISC 芯片研究发生在 80 年代初甚至更早。X86 之所以能超越 Alpha,是因为 DEC 当时经营不善,无法投入所需的资金。

          > 因此,没有理由在 ISA 中向用户公开它。

          除了隐藏实现成本高昂之外。

          如果给两个相同的团队同样多的钱,哪个团队的芯片速度更快?一个团队做的是简单的 RISC 指令集,还是一个团队做的是复杂的 CISC 指令集?还是将复杂的 CISC 指令集转化为底层更简单指令集的团队?

          当然,对于英特尔来说,他们拥有向后的可比性,所以他们必须做他们必须做的事情。他们只是很幸运,能够比其他竞争对手投入更多。

          1. > 你让学生设计的芯片性能超过了工业核心,这需要庞大的团队和巨额投资。

            大家记得感谢我们的变性女英雄索菲-威尔逊(Sophie Wilson,CBE)。

          2. > 如果给两个相同的团队同样多的钱,哪个团队的芯片速度更快?

            这取决于金额。如果少于一定数额,RISC 设计会更快。如果高于这个数额,两种设计的性能差不多。

            我的意思是,看看 ARM:他们也将指令解码为微操作,并在其高性能型号中缓存这些指令。RISC 所带来的是在低端市场上具有竞争力的能力,以及简单的实现方式。这就是为什么我们永远不会看到例如 堆栈式机器–没有外露的通用寄存器,但堆栈具有灵活的寻址模式,甚至像[SP+[SP+12]]那样;堆栈被镜像到隐藏的寄存器文件上,该文件被用作 “L0 ”高速缓存,巧妙地解决了寄存器窗口本应解决的问题。这就是为什么 System/360 没有选择这种设计,尽管 IBM 曾认真考虑了半年之久–他们后来发现底层机器的速度会慢得令人无法接受,于是他们采用了 “寄存器加基数加偏移寻址内存 ”的设计。

          3. 一切都很好,除了 Itanium 的出现,它与您列出的一切都背道而驰……?

            1. Itanium 并不是什么合理的 RISC,而是 “VLIW”。这给编译器带来了大量不必要的复杂性,却没有带来节省。

        3. 如果不考虑 CISC 芯片有独立的前端将复杂指令分解为类似 RISC 的内部指令集从而模糊了两者的区别,那么更多的 RISC 指令集确实倾向于在性能和功耗上取胜,主要原因是指令集有固定的宽度。这意味着您可以获取一行高速缓存和 4 字节指令,并行解码 32 条指令,而 x86 的可变性使得保持超标量流水线满载变得更加困难(它的解码器明显更加复杂,以试图仍然提取并行性,这进一步减慢了它的速度)。这在 ARM(也许还有 RISCV?)上要复杂一些,因为它有两种宽度,但即便如此,在实践中也更容易提取性能,因为 x86 的宽度可以是 1-4 字节(或 1-8,记不清了),这就很难找到并行的边界指令。

          苹果公司在性能/瓦数上压倒 AMD 和英特尔是有原因的,这不仅仅是因为他们采用了更新的晶圆厂工艺(这也是为什么 AMD 和英特尔完全无法推出其芯片的移动 CPU 变体)。

          1. x86 指令长度从 1 到 15 不等。

            > 一行高速缓存和 4 字节指令可以并行解码 32 条指令。

            实际上,ARM 处理器最多可并行解码 4 条指令;英特尔和 AMD 也是如此。

            1. 苹果的 m1 芯片是 8 宽。AMD 和英特尔的最新芯片也在做比 4 宽更花哨的事情

              1. 有阅读资源吗?我很想更好地了解他们为获得更好的解析度而使用的技术。我能想到的最明显的解决方案是,他们会尝试强制启动执行每一个可能的边界,并依靠它解码无效指令或延迟锁存结果,直到确认它是一个有效的指令边界。这通常就是这种技术吗?当然,这种技术的挑战在于,相对于一开始就没有那么多幻象可能性的架构,你有可能在幻象上浪费能量和执行单元。

      2. 但我们把 RISC 的梦想定义为:通过具有极小指令集的内核来实现效率、性能和低成本?

        1. 不是小指令集,而是简化指令集。RISC 的主要诀窍是减少寻址模式的数量(例如,不使用内存间接指令),并将每条指令的内存操作数减少到 0 或 1。将指令编码空间用于更多寄存器。

          现存的 CISC、x86 和 z390 是最不 CISC 化的 CISC。现存的 RISC,如 arm 和 power,是最没有 RISC 特征的 RISC。

          RISC V 在指令集设计的某些方面是一种奇怪的回溯。

          1. 实事求是地说,这与商业模式有关。POWER 过去和现在都得到了 IBM 的支持。ARM 在移动领域胜出。这是否意味着 POWER 和 ARM 比 MIPS、SPARC、PA-RISC、Am29000 和 i860 更好?我不这么认为。

        2. 如果增加更多的指令会对效率、性能、成本和复杂性产生负面影响,那么没有人会去做。

          1. 也许现在是这样,但在过去,一些指令的存在主要是为了让汇编编程更方便。

            在 SPARC 等最 RISC 的 RISC 体系结构中,汇编编程是一件非常麻烦的事情。下面是 https://www.cs.clemson.edu/course/cpsc827/material/Code%20Ge 中的一个例子…..:

            – 所有分支(包括下面由 CALL 引起的分支)都在执行下一条指令后发生。

            – 紧接分支后的位置是 “延迟槽”,在该位置找到的指令是 “延迟指令”。

            – 如果可能,在延迟槽中放置一条有用的指令(无论是否执行条件分支都可以安全执行的指令)。

            – 如果不行,则在延迟槽中放置一条 NOP 指令。

            – 切勿在延迟槽中放置任何其他分支指令。

            – 不要在延迟槽中使用 SET(实际上只有一半)。

            1. 延迟槽就是个黑科技。ARM 从不需要它们。

          2. 只有当解码器的复杂性/效率是你的瓶颈时才需要

    5. 在这种特殊情况下,他们试图在 RISCV5 上运行为 x86_64 编译的代码。我们只需要增加几条指令就能实现特性平价”,这种需求来自于试图运行已经为架构编译过的代码,而这些代码中包含了所有这些额外的指令。

      从理论上讲,如果为 RISC 编译原始源代码,就会得到一个完全二进制的代码,而不需要这些特定的指令。

      实际上,我怀疑是否有人会真正为 RISCV5 编译这些游戏。

    6. 我看到的解释是“(精简指令)集计算机”–指令简单,不一定少。

    7. 除了最微不足道的微控制器和实验设计之外,按照最初对 RISC 的理解,没有任何 RISC 芯片。当我们能够在芯片上安装 100 万个、1 亿个等晶体管时,RISC 的合理性就不复存在了。现在,所有被称为 “RISC ”的芯片都包含矢量、媒体、加密、网络、FPU 等指令。有人可能会说,RISC 设计的某些元素(正交指令编码、众多寄存器等)使特定芯片成为 RISC 芯片。但它们确实不是 RISC 字面概念的实例。

      对我来说,整个 RISC-V 的兴趣都只是市场营销。作为最终用户,我不会自己制造芯片,我也想不出有什么特别的理由让我关心一台机器是采用 RISC-V、ARM、x86、SPARC 还是 POWER。最终,我的成本将取决于市场规模和性能。设计的许可成本不会转嫁到我这个客户身上。

  6. 截图中显示的 31GB 内存明显超过了上述开发板的最大规格。他们是不是用了别的东西?

    1. 先锋,一块老板。

      请注意,如今,采用 RVA22 和 RVV 1.0、拥有多个更快内核的最新选择是更好的主意。

  7. 这就是 86Box?我觉得重温当年买 Amstrad PC1512 时的情景很有趣,我添加了两块 500MB 的硬盘,并将 128KB 内存扩展到 640KB,这让事情变得有趣多了。当时我只有两张 360KB 软盘,几年后才增加了一张 32MB 硬卡。我还有 Borland TurboPascal 和 Zortech C。有趣的时光

    1. 不,是 Box64,一个完全不同的项目。

      (不过我还记得我有一台 Amstrad PC1512:D)

      1. 一旦我弄到合适的 RISCV 硬件,试用 Box64 将会很有趣。我用过 RISCV 微控制器,它们很好用。

  8. 我在想,将来会不会有这样的系统:它是由几个大的 RISC-V CPU 组成,然后由一堆小的 RISC-V CPU 组成的 “GPU”(带有适当的向量–事实上,还有一个问题,传统的向量,而不是打包的 SIMD,在 GPU 中能派上用场吗?)

  9. 另一个在技术上令人印象深刻的是 Switch 移植的《巫师 3》,它运行得非常好。这说明了优化的作用有多大,也说明了 PC 上有多少资源被糟糕的优化所浪费。

    1. 由于使用了质量更低的纹理和 3D 模型,因此资产占用的内存更少。这不是苹果与苹果之间的比较,而且当屏幕上显示的内容范围大不相同时,你也无法真正声称 PC 上的优化不好。

    2. 如果你愿意将渲染分辨率设为 720p(脱机 540p),设置低于最低,并调用 ~30 FPS,那么你也可以在最低配置的 PC 上同样运行《巫师 3》。

  10. 我希望他们能将这种 ISA 级别的反馈信息反馈给 RVI 的人员

    1. 标量效率 SIG 已经在讨论位域插入和提取指令。

      我们昨天[1]发现,文章中的例子已经可以用四条 risc-v 指令完成,只是在实现上比较麻烦:

       # a0 = rax,a1 = rbx
          slli t0, a1, 64-8
          rori a0, a0, 16
          加 a0、a0、t0
          rori a0, a0, 64-16
      

      [1] https://www.reddit.com/r/RISCV/comments/1f1mnxf/box64_and_ri

      1. 这招不错,事实上,用 4 条指令就能达到提取/插入的效率,而且适用于所有 ADD/SUB/OR/XOR/CMP 指令(不适用于 AND),除非源寄存器是高字节寄存器。不过,在这种情况下,如果代码生成不是很好,也不是什么大问题:编译器实际上不会生成对这些寄存器的访问,虽然旧的 16 位汇编代码有很多这样的访问,但它们是为在运行频率为 4-20 MHz 的处理器上运行而设计的。

        标志计算和条件跳转是最大的优化机会所在。Box64 使用多路解码器计算标志的有效性信息,然后逐个计算标志。QEMU 则尝试存储原始操作数,然后懒散地计算标志。这两种方法各有利弊…

        1. > 除非源寄存器是高字节寄存器

          这只是多了一条指令,在与上述指令完全相同的指令之前,将 AH、BH 等操作数右对齐。

          是的,64 位代码的编译器不会生成这样的指令。事实上,由于 “部分寄存器更新停滞 ”的原因,90 年代中期奔腾 Pro、P II、P III 等产品中的 OOO 指令一出现,编译器就开始避免使用这些指令了。

        2. 实际上,Box64 也可以存储操作数,以便以后进行计算,这取决于接下来要做什么…

      2. 作者在此,我们采用了这种方法作为 box64 的快速通道:https://github.com/ptitSeb/box64/pull/1763,非常感谢!

    2. 这些都不是新东西。都不是。

      事实上,比特字段提取是一个非常明显的疏忽,它是我最喜欢的 RISCV ISA 是多么愚蠢的例子(第 2 个例子是缺乏合理的寻址模式)。

      事实上,一些较好的 RISCV 设计会采用自定义实例来实现这一点,例如 Hazard3 中的 BEXTM: Hazard3 中的 BEXTM: https://github.com/Wren6991/Hazard3/blob/stable/doc/hazard3….

      1. 哇,还有人不相信 RISC-V ISA 是 “完美的”!我很好奇:关于比特字段提取的讨论进展如何?因为这看起来确实是一个明显的疏忽,应该添加为 “标准扩展”。

        你怎么看

        1)使用 C 扩展的未对齐 32 位指令?

        2)算术指令没有 “溢出陷阱”?MIPS 就有

        1. 在我看来,他们犯了一个错误,就是不允许即时数据跟随指令。你可以在操作码中编码 8 位常量,但任何更大的数据都应适当支持即时数据。至于 C 扩展,我认为这也是劣质的,因为它是后来添加的。我希望在 10 年后,当情况真正稳定下来时,能对整个 ISA 进行重新编码。

          1. 你所说的主要问题是,这些经验教训都不是新的。在设计 ISA 之前,这些经验教训就已经广为人知了,因此如果设计者有意吸取过去的经验教训,他们完全有机会这样做。

        2. RISC-V 中对错位加载/存储的处理也是令人失望的一点:https://github.com/riscv/riscv-isa-manual/issues/1611,它偏向于为硬件开发人员提供方便和 “灵活性”,而不是为软件开发人员提供所需的实际保证。看来,MIPS 关于错位加载/存储指令的专利起到了负面作用。虽然该专利已于 2019 年到期,但我们似乎仍然无法摆脱目前的现状。

        3. Aarch64 在这方面做得很好。RISCV 试图同时做太多事情,结果可想而知,什么都做不好。快速的大内核应该坚持使用固定大小的实例,以加快解码速度。你总是知道 instrs 从哪里开始,每个缓存行都有一个整数的 instrs。微控制器内核可以使用压缩的 instrs,因为这对它们很重要,而尝试并行编解码 instrs 对它们并不重要。试图用一种方法涵盖所有情况是愚蠢的。

          2. 也没人在 mips 上使用它,所以它可能没什么用。

          1. > 快速大核应该坚持使用固定大小的 instrs,以提高解码速度。

            但能快多少呢?RISC-V 的解码并不像 x86 那样疯狂,只需查看第一个字节就能知道指令的长度(如果仅限于 16 位和 32 位指令,则查看前两位;如果支持 48 位指令,则查看 5 位;如果支持 64 位指令,则查看 6 位)。也就是说,解码器的串行部分非常非常小。

            对可变长度指令的更大抱怨是可能出现的指令错位,这与高速缓存线的关系并不好(一条指令可能从一条高速缓存线开始,在下一条高速缓存线结束,这让硬件变得更加棘手)。

            即使在大内核上,压缩指令也有一个优势:减少指令缓存的压力,相应地减少缓存缺失。

            因此,我并不清楚固定大小指令是否是大内核的最佳选择。

            1. 另一个反对 C 语言扩展的理由是,它使用了一大块操作码空间,而这些空间可能更适合用于其他 32 位指令扩展。

              1. 只使用 32 位和自然对齐的 64 位指令是否比使用较少的 32 位但 16/48/64 位指令更好?

                我认为孰优孰劣尚不明确。在我看来,48 位指令有很大的潜力,它们的代码密度比自然对齐的 64 位指令要好,而且它们能比 32 位指令编码更多的代码。(43位编码的2/3到3/4)

                基本上有两种设计理念:

                1. 32 位指令和 64 位自然对齐指令

                2. 16/32/48/64 位指令,16 位对齐

                实现的复杂性值得商榷,但似乎更倾向于方案 1:

                1:你需要把指令分解成 uops,因为你的 32 位指令需要做更复杂的事情

                2:需要找到指令起始点,并处理跨越缓存行的解码指令

                对整个设计的影响有多大还不清楚。

                寻找指令起始点意味着需要在整个解码宽度上传播几个比特,但破解也需要类似的东西。想想看,如果你能处理 8 个 uops,那么这些指令可以来自前 4 个被破解为每个 2 个 uops 的指令,也可以来自 8 个不需要被破解的指令,以及介于两者之间的所有指令。如果采用裂解,在流水线中执行时就有更大的自由度,但仍必须能够处理。

                最终,两者都需要跨高速缓存行解码以提高性能,但其中一个需要处理跨高速缓存行的指令分割。在我看来,这对验证复杂性的影响可能比实际实现的影响更大,但我还没有足够的资格知道这一点。

                如果两种方案都适合高性能实现,那么这就是一个关于取舍和 ISA 演进的问题。

                1. 还有一种中间方案,即要求用 16 位 NOP 填充 16/48 位序列,使其与 32 位对齐。我同意,目前还不清楚 C 扩展是否是个好主意(V 扩展也是如此)。

                  1. C 扩展的作者确实考虑过要求对齐/填充以防止出现 32 位指令错位的问题,但他们特别提到拒绝了这一要求,因为这会耗尽所有代码大小的节省。

                    1. 他们是否具体分析了在高速缓存行基础上进行对齐的问题?

                    2. 这就需要在 ABI 中指定缓存行大小,而这是一个有点奇怪的 uarch 细节。虽然 64 字节是大型应用处理器的常规大小,而且已经存在了很长时间,但我并不希望将其作为一项要求。

                    3. 但这绝对值得分析。

                      看看需要多大的数据块才能获得 90% 的压缩效益,等等。

                    4. 这对编译器来说似乎很困难。

                    5. 其实不然。大多数现代 x86 编译器已经将跳转目标与高速缓存行边界对齐,因为这对 x86 有很大帮助。所以这是可行的。如果将每个函数编译成一个部分(常用),那么链接器就可以很容易地将它们对齐到 64 或 128 字节。代码大小会增加(但可以玩俄罗斯方块,通过打包函数来减少代码大小)

            2. 坦率地说,在高性能 CPU 内核中,压缩指令没有任何优势,因为错位指令可能会跨越内存页边界,从而产生内存故障,可能会刷新 TLB,如果内存页不在内存中,则需要进行 I/O 操作。这比跨越高速缓存行还要糟糕。当两者同时发生时,会造成双重打击。

              一种建议的解决方案是用 NOP 填补空白,但这样编译器就必须跟踪页面对齐情况,而如果系统支持不同大小的页面(普通页面和超大页面),这种方法是行不通的。

              最好的解决办法也许是在针对高性能内核时忽略压缩指令,并将它们的使用限制在它们应该使用的地方:省电或低性能微控制器。

              1. 页面交叉影响的情况微乎其微–在 4096B 页面和 100% 非压缩指令(但仍有 50% 的时间存在错位)的情况下,2048 条指令中只有一条会受到影响。

                I/O 的可能性绝不仅限于压缩指令。如果跨页指令是填充的,那么无论如何都需要对第二页进行错误处理。重要的是这段代码所需的代码页数,也就是代码大小。

                唯一有可能产生影响的情况就是跨缓存线。

                而且我认为高性能内核无论如何都会有一些内部指令缓冲区,用于进行跨取舍块指令融合等。

              2. > 有一种建议的解决方案是用 NOP 填补空白,但这样编译器就必须跟踪页面对齐情况,而如果系统支持不同大小的页面(普通页面和超大页面),这种方法无论如何都行不通。

                如果是在链接器中,那么跟踪页面听起来是可行的。

                你不需要关心多种页面大小。如果以最小页面大小,甚至以 1KB 为界进行填充,那么 NOP 的数量就微乎其微了。

          2. 固定大小的指令并非绝对必要,但保持自然对齐更好,即使这意味着少用一些 C 语言指令。32 位指令可以跨页,这一点尤其令人头疼。

          3. >2.在 mips 上也没人用它,所以它可能没什么用。

            当然,但当时 Rust、Zig 还不存在,这两种语言都有检测整数溢出的模式。

      2. Bitfield-extract 正在讨论未来的扩展。例如,高通公司正在敦促添加该功能。

        在此期间,可以通过两次移位来实现:左移到 MSB,然后向右填充零或符号位。至少有一种正在开发中的内核(SpaceMiT X100)可以将这两种移位融合到一个 µop 中,也许有些内核已经做到了。

        不过,我也看到有一个核心(仙山南湖)在 B 扩展中将成对的 RVI 指令融合为一条指令,以便能够更快地运行为不带 B 的 CPU 编译的旧二进制文件。用硬件来解决这个问题,以避免重新编译……我觉得有点落后。

  11. 我对这个生态系统不是很熟悉,但我曾在 RPi4 上用它通过 wine 运行过一些游戏。

    我想知道,如今的情况如何?这是 ARM x86 兼容性的领先项目吗?随着 ARM 架构在消费平台上的日益普及,我猜像 Valve 这样的公司会有兴趣投资这类翻译层。

  12. 笑,我是反其道而行之。

    由于 RISC-V ISA 在全球范围内是免版税的,而且非常好用,我正在编写基本的 rv64 汇编,在 x86_64 硬件上用 linux 内核进行解释。

    我并没有刻意追求 “编译器”,因为这确实是在等待性能卓越的桌面(又称大型)rv64 硬件实现。

  13. 我曾经在 PocketCHIP 上使用过 GL4ES。我每天都在上网本上使用它,以提高一些 GL 2.1 游戏的性能。

  14. Box86 太棒了,我用它在免费的 Oracle 实例(ARM64)上运行 x86-64 蒸汽游戏(服务器)。

  15. 我记得在伯克利 CS61C 学习 RISC-V。有人来自伯克利吗?

      1. 哦,真的,我还真不知道。我也不知道。那门课程是开源的。

  16. > x86 指令集非常非常大。据粗略统计,ARM64 后端总共实现了 1600 多条 x86 指令,而 RV64 后端实现了大约 1000 条指令。

    这简直太疯狂了,也让我们彻底明白了为什么我们需要 RISC-V。

    1. 我认为 1600 是衡量此类问题的一个粗略指标。请记住,这些指令所能接受的形式参数数量是有限的:例如,16 条名义上不同的指令更容易被理解/记忆为一条带有隐含 4 位标志的指令。显然,英特尔 ISA 中存在大量的遗留问题,以及一些值得商榷的决定,我并不想抹杀 RISC 的魅力(例如,围绕这些 “伪参数化 ”指令存在大量尚未解决的编译器错误)。但是,看到 “1600 ”很容易让人联想到 “荒谬的臃肿”,而实际上,它具有一定的连贯性和系统性–更重要的是,对于性能高度敏感的工作来说,它显然是必要的。

      1. > 对性能高度敏感的工作显然是必要的

        显然有必要将可比性追溯到 80 年代。拥有 10 代不同的 SIMD 显然是必要的。拥有多个不同的浮点系统显然是必要的。

    2. 如果一个疯狂的指令集能为我们带来更高的性能,并使 CPU 和编译器的设计变得更加复杂,这也许是一个可以接受的权衡。

      1. 但事实并非如此。

        x86 拥有 50 年的巨额持续投资。英特尔的销售额超过了所有 RISC 供应商的总和,达到了 100 比 1,因为他们拥有 PC 业务。

        当苹果开始认真投资 ARM 时。他们就能与 x86 笔记本电脑相媲美。

        RISC-V 也将如此。

    3. 我希望有人能制作一个专门转换指令和编写测试的 GPT 微调器。如果让它阅读大量的 x86 文档和 risc v 文档,很多工作都可以自动完成。

    4. 并非如此。RISC-V 的优势不在于 “精简指令集 ”部分,而在于开放式 ISA 部分。小指令集实际上有几个缺点。它意味着你的二进制更大,因为 x86 中的单个操作在 RISC-V 中变成了多个,这意味着更多的内存带宽和缓存被指令占用,而不是数据。

      实际上,现代 CPU 非常擅长将操作分解为微操作。能够在微代码或硅片中实现复杂操作的灵活性对 CPU 设计人员来说至关重要。

      x86 中是否存在大量传统的垃圾?是的。去掉它们是否能显著提高性能上限?可能不会。

      RISC-V 的真正优势在于任何人都可以使用它。它使 ISA 民主化。没有人需要为使用它支付许可费,他们只需构建自己的 CPU 设计就可以了。

      1. > 现代 CPU 在将运算分解为微运算方面表现出色。

        最大的非顺序 CPU 实际上非常依赖于高性能解码,这种解码可以使用多个硬件单元并行执行。在这种情况下,从简化指令集入手,减少遗留包袱是一个优势。在 64 位 RISC ISA 中,RISC-V 在支持压缩指令方面也是独一无二的,这使其代码密度可与 x86 相媲美,而解码的简易性却大大提高(例如,它只需读取几个位就能确定哪些 insns 是 16 位长度,哪些是 32 位长度)。

      2. > 意味着二进制更大 ….,意味着更多的内存带宽和高速缓存

        但实际上并非如此。

        > 消除二进制会显著提高性能上限吗?可能不会。

        不会,但会大幅减少达到性能上限所需的投资。

        假设你有两个团队,每个团队获得的资金数额相同。然后要求他们制造性能最高的规格兼容芯片。哪个团队能在 99% 的情况下获胜?

        > 能够在微代码或硅片中实现复杂操作的灵活性对 CPU 设计师来说至关重要。

        如果你愿意,可以在 RISC-V 芯片上添加微代码,只是大多数人不愿意这么做。

        > RISC-V 的真正好处是任何人都可以使用它。

        没错,但它的指令集也比 x86 好得多 -_-

      3. >这意味着二进制更大

        前提是错误的,因为大小工具显示 RVA20(RV64GC) 二进制文件已经是 64 位架构中最小的。

        随着 RVA22 中 B 等新扩展的出现,代码变得越来越小(而不是越来越大)。

        最近,将 rv32 与以前的最佳版本(thumb2)进行比较时,32 位也是如此。但在此之前,两者已经相当接近了。

  17. 游戏中 >15 帧/秒

    哇……这大大超出了我的预期。硬件的好时代即将到来

  18. “可以运行《星露谷》等游戏,但对于其他更严肃的 Linux 游戏来说还不够”。

    嘿!;-)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注