用中国银行Visa外币卡 绑定google cloud时,会要求你验证六位数验证码:

收到了银行的短信,但是并没有在中国银行APP里看到对应账单:

这个时候,你可以打开
https://support.google.com/cloud/contact/verify?hl=zh-Hans
发起申诉,如实填写即可。

用中国银行Visa外币卡 绑定google cloud时,会要求你验证六位数验证码:

收到了银行的短信,但是并没有在中国银行APP里看到对应账单:

这个时候,你可以打开
https://support.google.com/cloud/contact/verify?hl=zh-Hans
发起申诉,如实填写即可。

文章由 ds-r1 翻译自 https://github.com/tevador/RandomX/blob/master/doc/design.md
为了最小化专用硬件的性能优势,工作量证明 (PoW) 算法必须通过针对现有通用硬件的特定功能来实现设备绑定。这是一项复杂的任务,因为我们必须针对来自不同制造商、具有不同架构的一大类设备。
通用处理设备分为两大类:中央处理器 (CPU) 和图形处理器 (GPU)。RandomX 针对 CPU,原因如下:
受 CPU 限制的工作量证明最基本的思想是,“工作”必须是动态的。这利用了 CPU 接受两种输入的事实:数据(主要输入)和代码(指定对数据执行什么操作)。
相反,典型的加密哈希函数 [3] 并不代表适合 CPU 的工作,因为它们唯一的输入是数据,而操作序列是固定的,可以通过专用的集成电路更高效地执行。
一个动态工作量证明算法通常可以由以下 4 个步骤组成:
1) 生成一个随机程序。
2) 将其翻译成 CPU 的原生机器码。
3) 执行该程序。
4) 将程序的输出转换为加密安全的值。
实际的“有用”的受 CPU 限制的工作在第 3 步中执行,因此必须调整算法以最小化其余步骤的开销。
早期动态工作量证明设计的尝试基于用高级语言(如 C 或 Javascript [4, 5])生成程序。然而,这效率非常低,主要有两个原因:
生成随机程序最快的方法是使用无逻辑生成器——简单地用随机数据填充缓冲区。这当然需要设计一种无语法的编程语言(或指令集),其中所有随机比特串都代表有效的程序。
这一步是不可避免的,因为我们不希望将算法限制在特定的 CPU 架构上。为了尽可能快地生成机器码,我们需要我们的指令集尽可能接近原生硬件,同时仍然足够通用以支持不同的架构。在代码编译期间没有足够的时间进行昂贵的优化。
实际的程序执行应尽可能利用 CPU 的多个组件。程序中应利用的一些功能包括:
第 2 章描述了 RandomX 虚拟机 (VM) 如何利用这些特性。
Blake2b [12] 是一种加密安全的哈希函数,专门设计为在软件中快速运行,尤其是在现代 64 位处理器上,其速度大约是 SHA-3 的三倍,并且可以以每字节输入约 3 个时钟周期的速度运行。该函数是用于对 CPU 友好的工作量证明的理想候选者。
对于以加密安全的方式处理更大量的数据,高级加密标准 (AES) [13] 可以提供最快的处理速度,因为许多现代 CPU 支持这些操作的硬件加速。有关 AES 在 RandomX 中使用的更多详细信息,请参见第 3 章。
当生成一个随机程序时,人们可能会选择仅在有利时才执行它。这种策略是可行的,主要有两个原因:
这些寻找具有特定属性程序的策略偏离了此工作量证明的目标,因此必须消除。这可以通过要求执行一系列 N 个随机程序链来实现,其中每个程序都是由前一个程序的输出生成的。然后使用最终程序的输出作为结果。
+---------------+ +---------------+ +---------------+ +---------------+
| | | | | | | |
输入 --> | 程序 1 | --> | 程序 2 | --> ... --> | 程序 (N-1) | --> | 程序 N | --> 结果
| | | | | | | |
+---------------+ +---------------+ +---------------+ +---------------+其原理是,在执行完第一个程序后,矿工必须要么承诺完成整个链(其中可能包含不利的程序),要么重新开始并浪费在未完成链上所付出的努力。附录 A 给出了这如何影响不同挖矿策略的哈希率示例。
此外,这种链式程序执行具有均衡整个链运行时间的好处,因为相同分布运行时间之和的相对偏差减小了。
由于工作量证明的用途是在无需信任的点对点网络中使用,网络参与者必须能够快速验证证明是否有效。这对工作量证明算法的复杂性设定了上限。特别是,我们为 RandomX 设定了一个目标:其验证速度至少要与它旨在取代的 CryptoNight 哈希函数 [15] 一样快。
除了纯粹的计算资源(如 ALU 和 FPU)之外,CPU 通常还可以访问大量 DRAM(动态随机存取存储器)[16] 形式的内存。内存子系统的性能通常被调整以匹配计算能力,例如 [17]:
为了利用外部内存以及片内内存控制器,工作量证明算法应访问一个大的内存缓冲区(称为“数据集”)。数据集必须满足:
使用 16 纳米工艺,单个芯片上可放置的最大 SRAM 超过 512 MiB;使用 7 纳米工艺,则超过 2 GiB [18]。理想情况下,数据集的大小至少应为 4 GiB。然而,由于验证时间的限制(见下文),RandomX 选择使用的数据集大小为 2080 MiB。虽然使用当前技术(2019 年的 7 纳米)理论上可以制造出具有此 SRAM 容量的单芯片,但至少在不久的将来,这种解决方案的可行性是值得怀疑的。
虽然要求专用挖矿系统拥有 >2 GiB 的内存来解决工作量证明是合理的,但必须为轻客户端提供使用更少内存量来验证证明的选项。
“快速”模式和“轻量”模式所需内存的比率必须谨慎选择,以免轻量模式适合挖矿。特别是,轻量模式的空间-时间(AT)乘积不应小于快速模式的 AT 乘积。AT 乘积的减少是衡量折中攻击 [19] 的常用方法。
鉴于前面章节描述的约束,快速验证模式和轻量验证模式之间最大可能的性能比经验证确定为 8。这是因为:
此外,选择 256 MiB 作为轻客户端模式所需的最大内存量。这个量对于 Raspberry Pi 等小型单板计算机来说也是可以接受的。
为了保持恒定的内存-时间乘积,快速模式的最大内存需求为:
8 * 256 MiB = 2048 MiB这可以进一步增加,因为轻量模式需要额外的芯片面积用于 SuperscalarHash 函数(参见规范第 3.4 章和第 6 章)。假设每个 SuperscalarHash 核心的保守估计面积为 0.2 mm²,DRAM 密度为 0.149 Gb/mm² [20],则额外内存为:
8 * 0.2 * 0.149 * 1024 / 8 = 30.5 MiB或四舍五入到最接近的 2 次幂为 32 MiB。快速模式的总内存需求可以是 2080 MiB,同时保持大致恒定的 AT 乘积。
本节描述 RandomX 虚拟机 (VM) 的设计。
RandomX 使用固定长度的指令编码,每条指令 8 字节。这允许在指令字中包含一个 32 位的立即数值。指令字位的解释经过选择,使得任何 8 字节字都是有效指令。这允许非常高效的随机程序生成(参见第 1.1.1 章)。
该 VM 是一个复杂指令集 (CISC) 机器,允许寄存器和内存寻址操作数。然而,每条 RandomX 指令仅转换为 1-7 条 x86 指令(平均 1.8 条)。保持指令复杂度相对较低以最小化具有定制指令集的专用硬件的效率优势非常重要。
VM 执行的程序采用由 256 条随机指令组成的循环形式。
VM 使用 8 个整数寄存器和 12 个浮点寄存器。这是在常见 64 位 CPU 架构中(x86-64 的架构寄存器数量最少)可以分配为物理寄存器的最大值。使用更多的寄存器会使 x86 CPU 处于劣势,因为它们将不得不使用内存来存储 VM 寄存器内容。
RandomX 使用所有具有高输出熵的基本整数运算:加法 (IADD_RS, IADD_M)、减法 (ISUB_R, ISUB_M, INEG_R)、乘法 (IMUL_R, IMUL_M, IMULH_R, IMULH_M, ISMULH_R, ISMULH_M, IMUL_RCP)、异或 (IXOR_R, IXOR_M) 和循环移位 (IROR_R, IROL_R)。
IADD_RS 指令利用了 CPU 的地址计算逻辑,并且可以被大多数 CPU(x86 lea,arm add)在单个硬件指令中执行。
由于整数除法在 CPU 中没有完全流水线化,并且可以在 ASIC 中做得更快,因此 IMUL_RCP 指令要求每个程序只进行一次除法来计算倒数。这迫使 ASIC 包含一个硬件除法器,同时不会在程序执行期间给它们带来性能优势。
循环移位指令在右移和左移之间分配,比例为 4:1。右移频率更高,因为某些架构(如 ARM)本身不支持左移(必须使用右移来模拟)。
该指令可以被支持寄存器重命名/移动消除的 CPU 高效执行。
RandomX 使用双精度浮点运算,大多数 CPU 都支持双精度,并且比单精度需要更复杂的硬件。所有操作都作为 128 位向量运算执行,这也得到所有主要 CPU 架构的支持。
RandomX 使用 IEEE 754 标准保证给出正确舍入结果的五种运算:加法、减法、乘法、除法和平方根。使用了标准定义的所有 4 种舍入模式。
浮点运算的域被分成“加法”运算(使用寄存器组 F)和“乘法”运算(使用寄存器组 E)。这样做是为了防止在将小数加到大数时加法/减法变成空操作。由于 F 组寄存器的范围限制在 ±3.0e+14 左右,因此加或减绝对值大于 1 的浮点数总是会改变至少 5 个小数位。
因为 F 组寄存器的有限范围允许使用更高效的定点表示(使用 80 位数字),所以 FSCAL 指令操作浮点格式的二进制表示以使这种优化更加困难。
E 组寄存器被限制为正值,这避免了 NaN 结果(例如负数的平方根或 0 * ∞)。除法仅使用内存源操作数,以避免被优化为乘以常数倒数。E 组内存操作数的指数设置为 -255 到 0 之间的值,以避免除以 0 和乘以 0,并增加可获得的数字范围。E 组寄存器值的近似范围是 1.7E-77 到 无穷大 (infinity)。
每个程序循环结束时浮点寄存器值的近似分布如下图所示(左图 - F 组,右图 - E 组):

(注:区间由左侧值标记,例如标记为 1e-40 的区间包含从 1e-40 到 1e-20 的值。)
F 组寄存器值在 1e+14 处的小数目是由 FSCAL 指令引起的,该指令显著增加了寄存器值的范围。
E 组寄存器覆盖了非常大的数值范围。大约 2% 的程序至少产生一个 无穷大 (infinity) 值。
为了最大化熵,同时也为了适应一个 64 字节的缓存行,浮点寄存器在每个迭代结束时使用异或 (XOR) 操作组合后存储到暂存器 (Scratchpad) 中。
现代 CPU 投入了大量的芯片面积和能量来处理分支。这包括:
为了利用推测设计,随机程序应包含分支。然而,如果分支预测失败,推测执行的指令将被丢弃,这会导致每次预测错误都会浪费一定量的能量。因此,我们应致力于最小化预测错误的分支数量。
此外,代码中的分支至关重要,因为它们显著减少了可以进行的静态优化的数量。例如,考虑以下 x86 指令序列:
...
branch_target_00:
...
xor r8, r9
test r10, 2088960
je branch_target_00
xor r8, r9
...XOR 操作通常会相互抵消,但由于分支的存在,不能进行优化,因为如果分支被采用,结果将会不同。同样,如果不是因为分支,ISWAP_R 指令总是可以被静态优化掉。
通常,随机分支的设计必须满足:
不幸的是,我们尚未找到在 RandomX 中利用分支预测的方法。因为 RandomX 是一个共识协议,所有规则必须预先设定,这包括分支规则。完全可预测的分支不能依赖于任何 VM 寄存器的运行时值(因为寄存器值是伪随机且不可预测的),因此它们必须是静态的,从而容易被专用硬件优化。
因此,RandomX 使用随机分支,其跳转概率为 1/256,并且分支条件依赖于一个整数寄存器值。这些分支将被 CPU 预测为“不跳转 (not taken)”。在大多数 CPU 设计中,除非发生跳转,否则这类分支是“免费的”。虽然这没有利用分支预测器,但与非推测性分支处理相比,推测性设计将看到显著的性能提升——更多信息参见附录 B。
分支条件和跳转目标的选择方式确保了在 RandomX 代码中不可能出现无限循环,因为控制分支的寄存器在重复的代码块中永远不会被修改。每条 CBRANCH 指令最多可以连续跳转两次。使用谓词执行 [22)] 来处理 CBRANCH 是不切实际的,因为大多数情况下分支不被采用。
CPU 利用多种利用代码指令级并行性的技术来提高性能。这些技术包括:
RandomX 受益于所有这些优化。详细分析见附录 B。
暂存器用作读写内存。其大小被选择为完全适合 CPU 缓存。
暂存器分为 3 级,以模拟典型的 CPU 缓存层次结构 [23]。大多数 VM 指令访问“L1”和“L2”暂存器,因为 L1 和 L2 CPU 缓存靠近 CPU 执行单元,并提供最佳随机访问延迟。从 L1 和 L2 读取的比例为 3:1,这与典型延迟的反比相匹配(见下表)。
| CPU μ架构 | L1 延迟 | L2 延迟 | L3 延迟 | 来源 |
|---|---|---|---|---|
| ARM Cortex A55 | 2 | 6 | - | [24] |
| AMD Zen+ | 4 | 12 | 40 | [25] |
| Intel Skylake | 4 | 12 | 42 | [26#Memory_Hierarchy)] |
L3 缓存要大得多,并且距离 CPU 核心更远。因此,其访问延迟要高得多,并可能导致程序执行停滞。
因此,RandomX 每次程序迭代仅执行 2 次对“L3”暂存器的随机访问(规范第 4.6.2 章中的步骤 2 和 3)。来自给定迭代的寄存器值被写入它们加载的相同位置,这保证了所需的缓存行已被移动到更快的 L1 或 L2 缓存中。
此外,从固定地址读取的整数指令也使用整个“L3”暂存器(规范表 5.1.4),因为重复访问将确保缓存行被放置在 CPU 的 L1 缓存中。这表明暂存器级别并不总是直接对应于相同的 CPU 缓存级别。
在 VM 执行期间,暂存器以两种方式被修改:
下图显示了在哈希计算期间对暂存器写入分布的示例。图像中的每个像素代表暂存器的 8 字节。红色像素代表在哈希计算期间至少被覆盖一次的暂存器部分。“L1”和“L2”级别在左侧(几乎完全被覆盖)。暂存器的右侧代表底部的 1792 KiB。其中只有大约 66% 被覆盖,但写入是均匀且随机分布的。

暂存器熵分析见附录 D。
程序每次迭代平均进行 39 次读取(指令 IADD_M, ISUB_M, IMUL_M, IMULH_M, ISMULH_M, IXOR_M, FADD_M, FSUB_M, FDIV_M)和 16 次写入(指令 ISTORE)到暂存器。每次迭代额外隐式读写 128 字节以初始化并存储寄存器值。每次迭代从数据集 (Dataset) 读取 64 字节数据。总计:
这接近 2:1 的读/写比率,这是 CPU 优化的目标。
由于暂存器通常存储在 CPU 缓存中,因此只有数据集访问会利用内存控制器。
RandomX 每次程序迭代从数据集随机读取一次(每个哈希结果 16384 次)。由于数据集必须存储在 DRAM 中,它提供了一个自然的并行化限制,因为 DRAM 每个存储体组 (bank group) 每秒最多只能进行约 2500 万次随机访问。每个独立寻址的存储体组允许约 1500 H/s 的吞吐量。
所有数据集访问读取一个 CPU 缓存行(64 字节)并且是完全预取的。执行规范第 4.6.2 章中描述的一次程序迭代所需的时间大约等于典型的 DRAM 访问延迟(50-100 ns)。
用于轻量验证和数据集构建的缓存 (Cache) 比数据集小约 8 倍。为了保持恒定的空间-时间乘积 (AT),每个数据集项由 8 次随机缓存访问构造而成。
因为 256 MiB 足够小,可以包含在片内,所以 RandomX 使用自定义的高延迟、高功耗混合函数(“SuperscalarHash”),它抵消了使用低延迟内存的优势,并且计算 SuperscalarHash 所需的能量使得轻量模式用于挖矿效率非常低下(参见第 3.4 章)。
使用少于 256 MiB 内存是不可能的,因为使用了具有 3 次迭代的抗折衷攻击 (tradeoff-resistant) 的 Argon2d。当使用 3 次迭代(遍数)时,将内存使用减半会使最佳折衷攻击 [27] 的计算成本增加 3423 倍。
AesGenerator1R 设计用于尽可能快地生成伪随机数据以填充暂存器。它利用了现代 CPU 中硬件加速的 AES。每输出 16 字节仅执行一轮 AES,这导致在大多数现代 CPU 中吞吐量超过 20 GB/s。
AesGenerator1R 在初始化状态足够“随机”时能提供良好的输出分布(参见附录 F)。
AesGenerator4R 使用 4 轮 AES 为程序缓冲区初始化生成伪随机数据。由于 2 轮 AES 足以实现所有输入位的完全雪崩效应 [28],AesGenerator4R 具有优异的统计特性(参见附录 F),同时保持非常好的性能。
该生成器的可逆特性不是问题,因为生成器状态总是使用不可逆哈希函数 (Blake2b) 的输出进行初始化。
AesHash 设计用于尽可能快地计算暂存器指纹。它将暂存器解释为一组 AES 轮密钥,因此相当于进行了 32768 轮的 AES 加密。最后执行额外的两轮以确保每个通道 (lane) 中的所有暂存器位都发生雪崩效应。
AesHash1R 的可逆特性不是问题,主要有两个原因:
SuperscalarHash 设计用于在 CPU 等待从 DRAM 加载数据时消耗尽可能多的功耗。170 个周期的目标延迟对应于通常 40-80 ns 的 DRAM 延迟和 2-4 GHz 的时钟频率。为具有低延迟内存的轻量模式挖矿设计的 ASIC 设备在计算数据集项时将被 SuperscalarHash 所瓶颈,并且其效率将被 SuperscalarHash 的高功耗所破坏。
平均 SuperscalarHash 函数总共包含 450 条指令,其中 155 条是 64 位乘法。平均而言,最长的依赖链是 95 条指令长。一个用于轻量模式挖矿的 ASIC 设计,具有 256 MiB 片内内存和所有操作 1 周期延迟,在假设无限并行化的情况下,平均需要 95 8 = 760 个周期来构建一个数据集项。它必须为每个项执行 155 8 = 1240 次 64 位乘法,这将消耗与从 DRAM 加载 64 字节相当的能量。
第 1.2 章描述了为什么链接 N 个随机程序以防止寻找“简单”程序的挖矿策略。RandomX 使用 N = 8。
让我们将 Q 定义为使用过滤策略时可接受程序的比率。例如 Q = 0.75 表示 25% 的程序被拒绝。
对于 N = 1,没有浪费的程序执行,唯一的成本是程序生成和过滤本身。下面的计算假设这些成本为零,唯一的实际成本是程序执行。然而,这是一种简化,因为在 RandomX 中程序生成并非免费(第一个程序生成需要完整的暂存器初始化),但它描述了攻击者的最佳情况。
对于 N > 1,第一个程序可以像往常一样被过滤,但在程序执行后,有 1-Q 的几率下一个程序应该被拒绝,这样我们就浪费了一次程序执行。
对于 N 次链接执行,只有 QN 的几率链中的所有程序都是可接受的。然而,在每次尝试找到这样一条链的过程中,我们将浪费执行一些程序。对于 N = 8,每次尝试浪费的程序数量等于 (1-Q)*(1+2*Q+3*Q2+4*Q3+5*Q4+6*Q5+7*Q6)(当 Q = 0.75 时约为 2.5)。
让我们考虑 3 种挖矿策略:
诚实的矿工,不拒绝任何程序 (Q = 1)。
使用优化的定制硬件的矿工,无法执行 25% 的程序 (Q = 0.75),但受支持的程序可以快 50% 执行。
可以执行所有程序的矿工,但拒绝链中第一个程序运行时间最慢的 25%。这为链中第一个程序提供了 5% 的性能提升(这与附录 C 中的运行时间分布相匹配)。
下表列出了上述 3 种策略针对不同 N 值的结果。列 N(I), N(II) 和 N(III) 列出了每种策略平均需要执行的程序数量以获得一个有效的哈希结果(这包括在拒绝链中浪费的程序)。列 Speed(I), Speed(II) 和 Speed(III) 列出了相对于策略 I 的平均挖矿性能。
| N | N(I) | N(II) | N(III) | Speed(I) | Speed(II) | Speed(III) |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1.00 | 1.50 | 1.05 |
| 2 | 2 | 2.3 | 2 | 1.00 | 1.28 | 1.02 |
| 4 | 4 | 6.5 | 4 | 1.00 | 0.92 | 1.01 |
| 8 | 8 | 27.0 | 8 | 1.00 | 0.44 | 1.00 |
对于 N = 8,策略 II 的执行速度不到诚实矿工的一半,尽管对选定程序具有 50% 的性能优势。策略 III 的微小统计优势在 N = 8 的情况下可以忽略不计。
如第 2.7 章所讨论的,RandomX 旨在利用现代高性能 CPU 的复杂设计。为了评估超标量、乱序和推测执行的影响,我们进行了简化的 CPU 模拟。源代码可在 perf-simulation.cpp 中找到。
模型 CPU 使用 3 级流水线来实现每周期 1 条指令的理想吞吐量:
(1) (2) (3)
+------------------+ +----------------+ +----------------+
| 指令取指 | | | | |
| + 解码 | ---> | 内存访问 | ---> | 执行 |
| | | | | |
+------------------+ +----------------+ +----------------+3 个阶段是:
请注意,这是一个乐观的短流水线,不允许非常高的时钟速度。使用更长流水线的设计将显著增加推测执行的好处。
我们的模型 CPU 包含两种组件:
超标量设计将包含多个执行或内存单元以提高性能。
模拟模型支持两种设计:
模拟模型支持两种分支处理类型:
模拟了以下 10 种设计,并测量了执行一个 RandomX 程序(256 条指令)的平均时钟周期数。
| 设计 | 超标量配置 | 重排序 | 分支处理 | 执行时间 [周期] | IPC |
|---|---|---|---|---|---|
| #1 | 1 EXU + 1 MEM | 按序 | 非推测性 | 293 | 0.87 |
| #2 | 1 EXU + 1 MEM | 按序 | 推测性 | 262 | 0.98 |
| #3 | 2 EXU + 1 MEM | 按序 | 非推测性 | 197 | 1.3 |
| #4 | 2 EXU + 1 MEM | 按序 | 推测性 | 161 | 1.6 |
| #5 | 2 EXU + 1 MEM | 乱序 | 非推测性 | 144 | 1.8 |
| #6 | 2 EXU + 1 MEM | 乱序 | 推测性 | 122 | 2.1 |
| #7 | 4 EXU + 2 MEM | 按序 | 非推测性 | 135 | 1.9 |
| #8 | 4 EXU + 2 MEM | 按序 | 推测性 | 99 | 2.6 |
| #9 | 4 EXU + 2 MEM | 乱序 | 非推测性 | 89 | 2.9 |
| #10 | 4 EXU + 2 MEM | 乱序 | 推测性 | 64 | 4.0 |
超标量、乱序和推测设计的好处显而易见。
运行时间数据在运行于 3.0 GHz 的 AMD Ryzen 7 1700(使用 1 个核心)上测量。测量程序执行和验证时间的源代码可在 runtime-distr.cpp 中找到。测量 x86 JIT 编译器性能的源代码可在 jit-performance.cpp 中找到。
下图显示了单个 VM 程序(在快速模式下)运行时间的分布。这包括:程序生成、JIT 编译、VM 执行和寄存器文件的 Blake2b 哈希。测得程序生成和 JIT 编译每个程序耗时 3.6 μs。

AMD Ryzen 7 1700 在快速模式下(使用 1 个线程)每秒可计算 625 个哈希,这意味着单个哈希结果耗时 1600 μs (1.6 ms)。这包括(大约):
这给出了 7.5% 的总开销(每个哈希花费在未执行 VM 上的时间)。
下图显示了使用轻量模式计算 1 个哈希结果所需时间的分布。大部分时间用于执行 SuperscalarHash 来计算数据集项(14.8 ms 中的 13.2 ms)。平均验证时间与 CryptoNight 算法的性能完全匹配。

执行 8 次程序后暂存器的平均熵是使用 LZMA 压缩算法近似的:
7z.exe a -t7z -m0=lzma2 -mx=9 scratchpads.7z *.spad生成的存档大小大约是暂存器文件未压缩大小的 99.98%。这表明在 VM 执行期间,暂存器保持了高熵。
SuperscalarHash 是 RandomX 用于生成数据集项的自定义函数。它对 8 个整数寄存器进行操作,并使用随机指令序列。大约 1/3 的指令是乘法。
下图显示了 SuperscalarHash 对改变输入寄存器单个比特的敏感性:

这表明 SuperscalarHash 对高位比特的敏感性相当低,对最低位比特的敏感性略有降低。对第 3-53 位(含)的敏感性最高。
在计算数据集项时,第一个 SuperscalarHash 的输入仅依赖于项编号。为了确保结果的良好分布,规范第 7.3 节中描述的常量经过选择,为范围 0-34078718(数据集包含 34078719 项)内的所有项编号提供唯一的第 3-53 位值。所有数据集项编号的所有初始寄存器值都经过检查,以确保每个寄存器的第 3-53 位是唯一的且没有冲突(源代码:superscalar-init.cpp)。虽然这对于从 SuperscalarHash 获得唯一输出并非严格必要,但这是一种安全预防措施,可以减轻随机生成的 SuperscalarHash 实例的非完美雪崩特性的影响。
AesGenerator1R 和 AesGenerator4R 都使用 TestU01 库 [30] 进行了测试,该库用于随机数生成器的经验测试。源代码可在 rng-tests.cpp 中找到。
测试对每个生成器采样约 200 MB(“SmallCrush”测试)、500 GB(“Crush”测试)或 4 TB(“BigCrush”测试)的输出。这远远超过 RandomX 中生成的量(AesGenerator4R 为 2176 字节,AesGenerator1R 为 2 MiB),因此测试失败并不一定意味着生成器不适合其用例。
当使用 Blake2b 哈希函数初始化时,该生成器通过了“BigCrush”套件中的所有测试:
$ bin/rng-tests 1
state0 = 67e8bbe567a1c18c91a316faf19fab73
state1 = 39f7c0e0a8d96512c525852124fdc9fe
state2 = 7abb07b2c90e04f098261e323eee8159
state3 = 3df534c34cdfbb4e70f8c0e1826f4cf7
...
========= BigCrush 的摘要结果 =========
版本: TestU01 1.2.3
生成器: AesGenerator4R
统计量数量: 160
总 CPU 时间: 02:50:18.34
所有测试均已通过即使初始状态设置为全零,该生成器也通过了“Crush”套件中的所有测试。
$ bin/rng-tests 0
state0 = 00000000000000000000000000000000
state1 = 00000000000000000000000000000000
state2 = 00000000000000000000000000000000
state3 = 00000000000000000000000000000000
...
========= Crush 的摘要结果 =========
版本: TestU01 1.2.3
生成器: AesGenerator4R
统计量数量: 144
总 CPU 时间: 00:25:17.95
所有测试均已通过当使用 Blake2b 哈希函数初始化时,该生成器通过了“Crush”套件中的所有测试。
$ bin/rng-tests 1
state0 = 67e8bbe567a1c18c91a316faf19fab73
state1 = 39f7c0e0a8d96512c525852124fdc9fe
state2 = 7abb07b2c90e04f098261e323eee8159
state3 = 3df534c34cdfbb4e70f8c0e1826f4cf7
...
========= Crush 的摘要结果 =========
版本: TestU01 1.2.3
生成器: AesGenerator1R
统计量数量: 144
总 CPU 时间: 00:25:06.07
所有测试均已通过当初始状态初始化为全零时,该生成器在“Crush”套件的 144 项测试中有 1 项失败:
$ bin/rng-tests 0
state0 = 00000000000000000000000000000000
state1 = 00000000000000000000000000000000
state2 = 00000000000000000000000000000000
state3 = 00000000000000000000000000000000
...
========= Crush 的摘要结果 =========
版本: TestU01 1.2.3
生成器: AesGenerator1R
统计量数量: 144
总 CPU 时间: 00:26:12.75
以下测试给出的 p 值超出了 [0.001, 0.9990]:
(eps 表示值 < 1.0e-300):
(eps1 表示值 < 1.0e-15):
测试 p 值
----------------------------------------------
12 BirthdaySpacings, t = 3 1 - 4.4e-5
----------------------------------------------
所有其他测试均已通过[1] CryptoNote 白皮书 - https://cryptonote.org/whitepaper.pdf
[2] ProgPoW: 低效的整数乘法 - https://github.com/ifdefelse/ProgPOW/issues/16
[3] 加密哈希函数 - https://en.wikipedia.org/wiki/Cryptographic_hash_function
[4] randprog - https://github.com/hyc/randprog
[5] RandomJS - https://github.com/tevador/RandomJS
[6] μop 缓存 - https://en.wikipedia.org/wiki/CPU_cache#Micro-operation_(%CE%BCop_or_uop)_cache
[7] 指令级并行 - https://en.wikipedia.org/wiki/Instruction-level_parallelism
[8] 超标量处理器 - https://en.wikipedia.org/wiki/Superscalar_processor
[9] 乱序执行 - https://en.wikipedia.org/wiki/Out-of-order_execution
[10] 推测执行 - https://en.wikipedia.org/wiki/Speculative_execution
[11] 寄存器重命名 - https://en.wikipedia.org/wiki/Register_renaming
[12] Blake2 哈希函数 - https://blake2.net/
[13] 高级加密标准 - https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
[14] 对数正态分布 - https://en.wikipedia.org/wiki/Log-normal_distribution
[15] CryptoNight 哈希函数 - https://cryptonote.org/cns/cns008.txt
[16] 动态随机存取存储器 - https://en.wikipedia.org/wiki/Dynamic_random-access_memory
[17] 多通道内存架构 - https://en.wikipedia.org/wiki/Multi-channel_memory_architecture
[18] Obelisk GRN1 芯片细节 - https://www.grin-forum.org/t/obelisk-grn1-chip-details/4571
[19] Biryukov 等人:内存困难函数的折衷密码分析 - https://eprint.iacr.org/2015/227.pdf
[20] SK 海力士 20nm DRAM 密度 - http://en.thelec.kr/news/articleView.html?idxno=20
[21] 分支预测器 - https://en.wikipedia.org/wiki/Branch_predictor
[22] 谓词执行 - https://en.wikipedia.org/wiki/Predication_(computer_architecture)
[23] CPU 缓存 - https://en.wikipedia.org/wiki/CPU_cache
[24] Cortex-A55 微架构 - https://www.anandtech.com/show/11441/dynamiq-and-arms-new-cpus-cortex-a75-a55/4
[25] AMD Zen+ 微架构 - https://en.wikichip.org/wiki/amd/microarchitectures/zen%2B#Memory_Hierarchy
[26] Intel Skylake 微架构 - https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client)#Memory_Hierarchy
[27] Biryukov 等人:用于加密货币和密码哈希的快速且抗折衷的内存困难函数 - https://eprint.iacr.org/2015/430.pdf 表 2,第 8 页
[28] J. Daemen, V. Rijmen: AES 提案: Rijndael - https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf 第 28 页
[29] 7-Zip 文件归档器 - https://www.7-zip.org/
[30] TestU01 库 - http://simul.iro.umontreal.ca/testu01/tu01.html

flatpak、系统包管理器(我这里是pacman)是两套不同的体系。
为了在尽可能多的发行版上跑起来,flatpak 担任了系统包管理器的部分职责,包括 N卡驱动的用户空间部分,在系统包管理器已经提供的前提下,flatpak 仍然会使用自己下载的用户空间驱动,例如 org.freedesktop.Platform.GL.nvidia-575-64-05。
在使用系统包管理器更新 Nvidia用户空间驱动、内核驱动后,flatpak 自己的的旧版用户空间驱动并不能和系统包管理器管理的新版内核驱动 匹配上。
这个时候 flatpak update 一下,让 flatpak 安装和 新版内核驱动 配对的 新版用户空间驱动 即可。
orreo@VC200 ~> flatpak update
正在查找更新…
ID 分支 操作 远程 下载
1. [✓] org.freedesktop.Platform.GL.nvidia-575-64-05 1.4 i flathub 260.3 MB / 316.0 MB
安装完成。
orreo@VC200 ~>参考:https://www.perplexity.ai/search/flatpakru-he-zhi-xing-gccao-zu-fjq6.G7mS4C3Qnh7kH_2kQ?1=d 、 https://blog.tingping.se/2018/08/26/flatpak-host-extensions.html
设计一个面向未来的 API 异常困难。本文档中的建议通过权衡取舍,以支持长期无缺陷的演进。
本文档是对 Proto 最佳实践的补充, 并非 Java/C++/Go 等 API 的规范。
⚠️ 注意
这些准则并非绝对,许多情况都有例外。例如, 若编写性能关键的后端服务,可能需牺牲灵活性或安全性换取速度。 本文旨在帮助您理解权衡取舍,做出适合自身场景的决策。
您的 proto 很可能被不了解原始设计思路的继承者使用。 请以新团队成员或系统知识有限的客户端视角, 为每个字段编写实用文档。
具体示例:
// 反面示例:启用 Foo 的选项
// 正面示例:控制 Foo 功能行为的配置
message FeatureFooConfig {
// 反面示例:设置功能是否启用
// 正面示例:必填字段,指示 Foo 功能是否对 account_id 启用。
// 若 account_id 未设置 FOO_OPTIN Gaia 位,必须为 false。
optional bool enabled;
}
// 反面示例:Foo 对象
// 正面示例:API 中暴露的 Foo (what/foo) 客户端表示
message Foo {
// 反面示例:Foo 的标题
// 正面示例:表示用户提供的 Foo 标题(未经标准化或转义)
// 示例标题:"Picture of my cat in a box <3 <3 !!!"
optional string title [(max_length) = 512];
}
// 反面示例:Foo 配置
// 次优方案:若最有用的注释仅是复述名称,不如省略注释
FooConfig foo_config = 3;用最简练的语言描述每个字段的约束、期望和解释规则。
可使用自定义 proto 注解(如示例中的 max_length), 详见自定义选项。 支持 proto2 和 proto3。
接口文档会随时间增长,冗长将降低清晰度。 当文档确实不清晰时,应修复它, 但需整体审视并追求简洁。
若客户端接口与磁盘存储使用相同的顶层 proto, 将引发隐患。随时间推移,更多二进制文件会依赖您的 API, 使其难以修改。您需要在不影响客户端的情况下自由变更存储格式。 通过分层代码,让模块分别处理客户端 proto、存储 proto 或转换逻辑。
为何? 您可能需要更换底层存储系统, 调整数据归一化/反归一化策略, 或发现部分客户端 proto 适合存入 RAM 而其他部分应落盘。
对于嵌套在请求/响应中的 proto, 分离存储层与传输层的必要性会降低, 这取决于您希望客户端与这些 proto 的耦合程度。
维护转换层虽有成本,但在拥有客户端 且需首次变更存储格式时,其价值会迅速显现。
若试图”需要时再分离”共享 proto, 由于分离成本高且内部字段无处存放, API 将积累客户端不理解或未经授权就依赖的字段。
通过独立的 proto 文件起步, 团队将明确添加内部字段的位置, 避免污染 API。早期可通过自动转换层 (如字节复制或 proto 反射)保持传输层 proto 完全一致。 Proto 注解也可驱动自动转换层。
例外情况:
google.type 或 google.protobuf), 可同时用于存储和 API注意:若实现日志系统或通用存储的 proto 包装器, 应让客户端消息尽可能透明地进入存储后端, 避免创建依赖枢纽。可考虑使用扩展或 通过 Web 安全编码将二进制序列化数据编码为字符串。
避免创建仅接收 Foo 的 UpdateFooRequest。
若客户端不保留未知字段,其 GetFooResponse 将缺少新字段, 导致往返数据丢失。部分系统不保留未知字段。 Proto2/proto3 实现会保留未知字段,除非显式丢弃。 通常公共 API 应在服务端丢弃未知字段以防止安全攻击 (例如垃圾未知字段可能在服务端未来将其用作新字段时引发故障)。
未明确文档化时,可选字段的处理存在歧义: UpdateFoo 会清空字段吗?当客户端未知该字段时会导致数据丢失。 不修改字段?客户端如何清空字段?两种方案均不理想。
客户端传递需修改的字段,服务端仅更新掩码指定字段。 掩码结构应与响应 proto 结构镜像: 若 Foo 包含 Bar,则 FooMask 应包含 BarMask。
例如,用 PromoteEmployeeRequest、SetEmployeePayRequest、 TransferEmployeeRequest 等替代 UpdateEmployeeRequest。
定制更新方法比通用更新方法更易监控、审计和防护。 其实现和调用也更简单。但过多定制方法会增加 API 认知负担。
本规则可规避文档中描述的诸多陷阱。
例如:通过消息包装重复字段, 可区分存储层的未设置状态与特定调用中的未填充状态。
共享的通用请求选项将自然遵循此规则, 读写字段掩码也由此衍生。
顶层 proto 应始终作为可独立扩展的其他消息的容器。
即使当前仅需单个原始类型,将其包装在消息中 也为后续扩展和类型共享提供清晰路径。例如:
message MultiplicationResponse {
// 反面示例:后续需返回复数时如何处理?
// 若 AdditionResponse 需返回相同多字段类型?
optional double result;
// 正面示例:其他方法可复用此类型,随服务功能扩展而演进
// (如添加单位、置信区间等)
optional NumericResult result;
}
message NumericResult {
optional double real_value;
optional double complex_value;
optional UnitType units;
}例外:若字符串(或字节)实际编码 proto 且仅在服务端构建解析, 可将续传令牌、版本令牌和 ID 作为字符串返回, 但字符串必须是结构化 proto 的编码(遵循下文准则)。
若字段确实仅描述两种状态(永久性而非短期),可使用布尔值。 通常枚举、整型或消息提供的灵活性更值得投入。
例如,返回帖子流时,开发者可能需根据当前 UX 原型 决定是否双栏渲染。即使当前仅需布尔值, 也无法阻止未来引入双行帖、三栏帖或四宫格帖。
message GooglePlusPost {
// 反面示例:是否跨双栏渲染
optional bool big_post;
// 正面示例:客户端渲染提示
// 客户端应据此决定渲染突出程度,缺省时使用默认渲染
optional LayoutConfig layout_config;
}
message Photo {
// 反面示例:是否为 GIF
optional bool gif;
// 正面示例:引用照片的文件格式(如 GIF/WebP/PNG)
optional PhotoType type;
}谨慎添加枚举状态。若状态引入新维度或隐含多重行为, 几乎必然需要新字段。
用 int64 作为对象 ID 很诱人,但应优先选择字符串。
这便于后续变更 ID 空间并减少冲突概率。 2^64 在现代已非足够大。
也可将结构化 ID 编码为字符串,鼓励客户端视其为不透明数据。 此时仍需 proto 支撑字符串,但序列化为字符串字段 (如 Web-safe Base64 编码)可从客户端 API 剥离内部细节。 具体遵循下文准则。
message GetFooRequest {
// 指定获取的 Foo
optional string foo_id;
}
// 序列化并 websafe-base64 编码到 GetFooRequest.foo_id
message InternalFooRef {
// 仅设置其一。已迁移的 Foo 使用 spanner_foo_id,
// 未迁移的使用 classic_foo_id
optional bytes spanner_foo_id;
optional int64 classic_foo_id;
}若自行设计 ID 字符串序列化方案,可能快速引发问题。 因此最好用内部 proto 支撑字符串字段。
网络传输效率低,增加 proto 消费者工作量, 且令文档读者困惑。客户端还需考虑编码细节: 列表是否逗号分隔?非信任数据是否正确转义?数字是否十进制? 更好的方案是让客户端发送实际消息或原始类型, 网络传输更紧凑,客户端更清晰。
当服务拥有多语言客户端时尤其严重, 每个客户端需选择正确解析器/构建器(或更糟——自行编写)。
通用原则:选择正确的原始类型。 参见 Protocol Buffer 语言指南中的标量类型表。
对 JavaScript 客户端,在 API 字段中返回 HTML 或 JSON 很诱人, 但这易导致 API 与特定 UI 绑定。三大风险:
除初始页面加载外, 通常应在客户端返回数据并用客户端模板构建 HTML。
若需在客户端可见字段编码不透明数据 (续传令牌、序列化 ID、版本信息等), 需文档化要求客户端视其为不透明数据。 始终使用二进制 proto 序列化,勿用文本格式或自定义方案。
定义内部 proto 保存字段(即使仅需一个字段), 序列化为字节后,用 Web-safe base64 编码到字符串字段。
罕见例外:若定制格式的紧凑性收益显著,可替代序列化方案。
暴露给客户端的 API 应仅描述系统交互方式。 添加其他内容会增加理解成本。
过去常在响应 proto 返回调试数据,现有更佳方案: RPC 响应扩展(或称”旁路通道”)可分离客户端接口与调试接口。
类似地,返回实验名称曾是日志便利手段 ——隐含约定是客户端在后续操作中回传这些实验。 当前推荐方案是在分析管道中进行日志关联。
例外情况: 若需实时持续分析且机器资源紧张, 日志关联成本过高时可预先反规范化日志数据。 如需日志数据往返传递,可作为不透明数据发送给客户端, 并文档化请求/响应字段。
注意:若需在每个请求中返回或往返隐藏数据, 您掩盖了服务的真实使用成本,这同样不利。
message FooQuery {
// 反面示例:若两次查询间数据变更,以下策略均可能导致结果遗漏
// 在最终一致性系统(如 Bigtable 存储)中,新旧数据交错出现很常见
// 且偏移量/分页方案均假设排序规则,丧失灵活性
optional int64 max_timestamp_ms;
optional int32 result_offset;
optional int32 page_number;
optional int32 page_size;
// 正面示例:灵活性!在 FooQueryResponse 中返回,
// 客户端下次查询时回传
optional string next_page_token;
}分页 API 最佳实践是使用不透明续传令牌(称 next_page_token), 由内部 proto 支撑(序列化后执行 WebSafeBase64Escape (C++) 或 BaseEncoding.base64Url().encode (Java))。内部 proto 可包含多字段, 关键是通过灵活性为客户端提供结果稳定性。
勿忘验证 proto 字段的不可信输入 (参见准则)。
message InternalPaginationToken {
// 追踪已见 ID:以续传令牌增大为代价实现完美召回
repeated FooRef seen_ids;
// 类似 seen_ids 策略,但用布隆过滤器节省字节(牺牲精度)
optional bytes bloom_filter;
// 合理初版方案。嵌入续传令牌可在不影响客户端的情况下变更
optional int64 max_timestamp_ms;
}message Foo {
// 反面示例:Foo 的价格和货币类型
optional int price;
optional CurrencyType currency;
// 更佳方案:封装 Foo 的价格和货币类型
optional CurrencyAmount price;
}仅高内聚字段应嵌套。若字段确实相关, 在服务内部传递时会更便捷。例如:
CurrencyAmount calculateLocalTax(CurrencyAmount price, Location where)若变更引入一个字段,但该字段后续可能有相关字段, 应预先放入独立消息以避免:
message Foo {
// 已弃用!使用 currency_amount
optional int price [deprecated = true];
// Foo 的价格和货币类型
optional google.type.Money currency_amount;
}嵌套消息的问题是:CurrencyAmount 可能在 API 其他位置复用, 但 Foo.CurrencyAmount 可能不会。最坏情况是复用 Foo.CurrencyAmount, 但 Foo 专属字段泄漏其中。
虽然松耦合 是系统开发的通用准则,但设计 .proto 文件时未必适用。 若两个信息单元高度相关(通过嵌套),可能有意义。 例如,若创建一组当前较通用的字段, 但预期未来添加专属字段,嵌套消息可阻止他人 在其他 .proto 文件中引用该消息。
message Photo {
// 反面示例:PhotoMetadata 可能在 Photo 范围外复用
// 因此不嵌套更易访问是优选
message PhotoMetadata {
optional int32 width = 1;
optional int32 height = 2;
}
optional PhotoMetadata metadata = 1;
}
message FooConfiguration {
// 正面示例:在 FooConfiguration 外复用 Rule
// 会与无关组件紧耦合,嵌套可避免此问题
message Rule {
optional float multiplier = 1;
}
repeated Rule rules = 1;
}// 推荐方案:使用 google.protobuf.FieldMask
// 替代方案一:
message FooReadMask {
optional bool return_field1;
optional bool return_field2;
}
// 替代方案二:
message BarReadMask {
// 需返回的 Bar 字段标签号
repeated int32 fields_to_return;
}若使用推荐的 google.protobuf.FieldMask, 可用 FieldMaskUtil (Java/C++) 库自动过滤 proto。
读取掩码为客户端设定期望,控制需返回的数据量, 并让后端仅获取必要数据。
可接受替代方案是始终填充所有字段(即隐式全 true 读取掩码)。 但随着 proto 增长,成本会上升。
最差模式是存在未声明的隐式读取掩码, 且根据填充方法不同而变化。此反模式会导致 通过响应 proto 构建本地缓存的客户端出现数据丢失。
当客户端写入后立即读取同一对象时, 期望得到所写内容(即使底层存储系统不保证此行为)。
服务端读取本地值后,若本地 version_info 低于预期, 将从远端副本读取最新值。通常 version_info 是 字符串编码的 proto, 包含数据中心和提交时间戳。
即使由一致性存储支撑的系统, 也常需令牌触发高成本的一致性读取路径, 而非每次读取均承担此开销。
反面模式示例:某服务中每个返回相同数据类型的 RPC, 却有独立选项指定最大评论数、支持的嵌入类型列表等。
临时方案会增加客户端填写请求的复杂性, 及服务端转换 N 个选项为通用内部选项的复杂性。 现实中的大量 bug 可追溯至此。
应创建独立的消息容纳请求选项, 并包含在每个顶层请求消息中。改进方案:
message FooRequestOptions {
// 字段级读取掩码。仅返回请求的字段。
// 客户端应仅请求所需字段以帮助后端优化
optional FooReadMask read_mask;
// 响应中每个 Foo 返回的最大评论数(垃圾评论不计入)。
// 默认不返回评论
optional int max_comments_to_return;
// 包含非支持类型嵌入的 Foo,将降级转换为此列表中的类型。
// 未指定列表时不返回嵌入内容。若无法降级转换,则不返回嵌入。
// 强烈建议客户端始终包含 EmbedTypes.proto 中的 THING_V2 类型
repeated EmbedType embed_supported_types_list;
}
message GetFooRequest {
// 读取的 Foo。若查看者无权访问或 Foo 已删除,响应为空但成功
optional string foo_id;
// 客户端必须包含此字段。若 FooRequestOptions 为空,
// 服务端返回 INVALID_ARGUMENT
optional FooRequestOptions params;
}
message ListFooRequest {
// 返回的 Foo。搜索召回率 100%,但子句越多性能影响越大
optional FooQuery query;
// 客户端必须包含此字段。若 FooRequestOptions 为空,
// 服务端返回 INVALID_ARGUMENT
optional FooRequestOptions params;
}尽可能保证操作的原子性,更重要的是保证 幂等性。部分失败后的重试不应破坏/重复数据。
有时出于性能需单个 RPC 封装多个操作。 部分失败时如何处理?若部分成功部分失败, 最佳方案是让客户端知晓两者详情。
通常建议将 RPC 设为失败状态, 在 RPC 状态 proto 中返回成功和失败的详情。 理想情况是:未感知部分失败的客户端仍能正确处理, 感知的客户端可获取额外价值。
通过单次往返查询多个细粒度数据的能力, 允许客户端组合所需内容,无需服务端变更即可支持更广的 UX 范围。
这对前端和中层服务器最相关。
许多服务提供自有批处理 API。
当Web/移动端客户端需进行两次有数据依赖的查询时, 当前最佳实践是创建新 RPC 避免客户端额外往返。
对移动端,几乎总值得通过捆绑两个服务方法为单一新方法 节省往返开销。对服务间调用,情况可能不同, 取决于服务性能敏感度和新方法的认知开销。
常见演进场景是单个重复字段需变为多个关联重复字段。 若以原始类型起步,选项有限:要么创建并行重复字段, 要么定义含值容器的新重复字段并迁移客户端。
若以重复消息起步,演进将变得简单。
// 描述照片增强类型
enum EnhancementType {
ENHANCEMENT_TYPE_UNSPECIFIED;
RED_EYE_REDUCTION;
SKIN_SOFTENING;
}
message PhotoEnhancement {
optional EnhancementType type;
}
message PhotoEnhancementReply {
// 正面示例:PhotoEnhancement 可扩展描述需更多字段的增强类型
repeated PhotoEnhancement enhancements;
// 反面示例:若需返回增强相关参数,
// 需引入并行数组(糟糕)或弃用此字段改用重复消息
repeated EnhancementType enhancement_types;
}设想需求:“需区分用户执行的增强与系统自动应用的增强”。
若 PhotoEnhancementReply 中是标量或枚举,支持此需求将更困难。
此原则同样适用于 map。若 map 值已是消息类型, 添加字段远比从 map<string, string> 迁移到 map<string, MyProto> 简单。
例外: 延迟敏感型应用会发现原始类型的并行数组比消息数组 构建/删除更快。若使用 packed=true(省略字段标签), 网络传输也更小。Proto3 中自动打包。 分配固定数量数组比分配 N 个消息开销更小。
在 Proto3 引入 Map 类型前, 服务常使用临时 KVPair 消息暴露数据。当客户端需要深层结构时, 最终需设计需解析的键/值(参见准则)。
因此,对值使用(可扩展的)消息类型是基础改进。
Map 已向后移植到 proto2,故使用 map<标量, **消息**> 优于自定义 KVPair1。
若需表示结构未知的任意数据, 请使用 google.protobuf.Any。
上层客户端可能包含重试逻辑。若重试的是变更操作, 用户可能遭遇意外:重复评论、构建请求、编辑等对任何人都不利。
避免重复写入的简单方案是允许客户端指定请求 ID(如内容哈希或 UUID), 服务端依此去重。
服务名称(即 .proto 文件中 service 关键字后的部分) 用途远超生成服务类名,使其重要性超乎预期。
棘手之处在于这些工具隐含假设服务名称在网络中唯一。 更糟的是它们使用非限定服务名(如 MyService), 而非限定名(如 my_package.MyService)。
因此,即使服务定义在特定包内, 也应采取措施防止服务名冲突。 例如 Watcher 可能引发问题, MyProjectWatcher 更佳。
请求/响应大小应有上限。建议约 8 MiB, 2 GiB 是多数 proto 实现的硬限制。 许多存储系统对消息大小有限制。
此外,无界消息会:
限制消息大小的方案: - 定义返回有界消息的 RPC,每次调用逻辑独立 - 定义操作单个对象的 RPC,而非操作客户端指定的无界列表 - 避免在字符串、字节或重复字段编码无界数据 - 定义长时运行操作。将结果存入支持可扩展并发读取的存储系统 - 使用分页 API(参见准则) - 使用流式 RPC
若开发 UI,另见准则。
RPC 服务应在边界检查错误,并向调用方返回有意义的状态错误。
以简单示例说明:
假设客户端调用无参数的 ProductService.GetProducts。 执行过程中,ProductService 获取所有产品, 并为每个产品调用 LocaleService.LocaliseNutritionFacts。
digraph toy_example {
node [style=filled]
client [label="Client"];
product [label="ProductService"];
locale [label="LocaleService"];
client -> product [label="GetProducts"]
product -> locale [label="LocaliseNutritionFacts"]
}若 ProductService 实现错误,可能向 LocaleService 发送错误参数导致 INVALID_ARGUMENT。
若 ProductService 草率返回错误,客户端将收到 INVALID_ARGUMENT(状态码跨 RPC 传播)。 但客户端未向 ProductService.GetProducts 传递任何参数! 该错误比无用更糟:将引发严重困惑!
正确做法:ProductService 应在 RPC 边界检查错误 (即其实现的 RPC 处理程序)。若自身收到无效参数, 应返回 INVALID_ARGUMENT;若下游收到无效参数, 应先将 INVALID_ARGUMENT 转为 INTERNAL 再返回。
草率传播状态错误将导致调试成本高昂的困惑, 甚至因服务转发客户端错误却不触发警报, 引发不可见的中断。
通用规则:在 RPC 边界仔细检查错误, 向调用方返回带合适状态码的有意义错误。 为准确传达意图,每个 RPC 方法应文档化其返回的错误码及场景。 方法实现应符合文档约定的 API 契约。
为每个 RPC 方法创建唯一的请求/响应 proto。 后期发现需变更顶层请求/响应时成本高昂。 包括”空”响应:应创建专属空响应 proto, 而非复用知名 Empty 类型。
复用消息时,创建共享”领域”消息类型供多个请求/响应 proto 包含。 应用逻辑应基于这些类型而非请求/响应类型编写。
这使您能独立演进方法请求/响应类型, 同时共享逻辑子单元的代码。
当重复字段为空时,客户端无法区分是服务端未填充, 还是底层数据确实为空(即重复字段无 hasFoo 方法)。
用消息包装重复字段是获得 hasFoo 方法的简单方案:
message FooList {
repeated Foo foos;
}更系统的方案是使用字段读取掩码。 若字段被请求,空列表表示无数据; 若未请求,客户端应忽略响应中的该字段。
最差方案是强制客户端提供完整替换列表。 此方案风险包括:不保留未知字段的客户端导致数据丢失; 并发写入导致数据丢失;即使无此问题, 客户端也需仔细阅读文档才能理解服务端解释逻辑 (空字段表示不更新还是清空?)。
解决方案1:使用允许客户端替换、删除或插入元素的重复更新掩码, 无需在写入时提供整个数组。
解决方案2:在请求 proto 中创建独立的追加、替换、删除数组。
解决方案3:仅允许追加或清空。 可通过消息包装重复字段实现: 若存在但为空的消息表示清空,否则重复元素表示追加。
尽量避免顺序依赖,这是额外的脆弱层。 最恶劣的顺序依赖是并行数组, 这会增加客户端解读结果的难度, 并使其在服务内部传递时变得不自然。
message BatchEquationSolverResponse {
// 反面示例:解值按请求方程顺序返回
repeated double solved_values;
// (通常)反面示例:solved_values 的并行数组
repeated double solved_complex_values;
}
// 正面示例:独立消息可扩展更多字段并被其他方法复用。
// 请求与响应间无顺序依赖,多重复字段间无顺序依赖
message BatchEquationSolverResponse {
// 已弃用,2014 年 Q2 前响应将继续填充此字段,
// 之后客户端必须改用下方的 solutions 字段
repeated double solved_values [deprecated = true];
// 正面示例:请求中的每个方程有唯一 ID,
// 包含在下方的 EquationSolution 中以便与解关联。
// 方程并行求解,解产生后加入此数组
repeated EquationSolution solutions;
}Android/iOS 运行时均支持反射,为此会将字段和消息的原始名称 以字符串形式嵌入应用二进制文件(APK/IPA)。
message Foo {
// 此字段将在 Android/iOS 泄露 Google Teleport 项目存在
optional FeatureStatus google_teleport_enabled;
}缓解策略:
strings 命令 将暴露 proto 字段名(参见 iOS Chrome 拆解)绝不可以此为借口用代号混淆字段含义。 要么堵漏,要么征得同意承担风险。
某些情况下可牺牲类型安全或清晰度换取性能。 例如,含数百个字段(特别是消息类型字段)的 proto 解析速度慢于字段少的 proto。 深度嵌套的消息仅因内存管理就会降低反序列化速度。 团队曾用以下技术加速反序列化:
本文翻译自:2025-7-17 API Best Practices | Protocol Buffers Documentation
含 map<k,v> 字段的 proto 的陷阱:勿在 MapReduce 中将其作为归约键。 Proto3 map 项的传输格式和迭代顺序未指定,导致不一致的分片结果。↩︎
不使用 all ways gpu 的 5. Set Compositor Specific Variables (Method 3),手动 配置全部应用程序使用 GPU 渲染,感觉上 BUG 少、更流畅:

之前把 all ways gpu 的方法3和 手动配置应用程序使用 GPU 渲染 方法组合使用,导致全屏时莫名奇妙地崩溃
orreo@VC200 ~> org.gnome.gitlab.somas.Apostrophe
Adding pride CSS class flag-disability
<点全屏>
(apostrophe:2): Gdk-WARNING **: 11:01:24.373: vkAcquireNextImageKHR(): A surface has changed in such a way that it is no longer compatible with the swapchain. (VK_ERROR_OUT_OF_DATE_KHR) (-1000001004)
<卡死>大部分 Wayland 原生应用点全屏都卡死崩溃,XWayland点全屏时界面被冻结
有的时候有问题,有的时候就完全正常。
换到本文方法(关闭 all ways gpu)后问题解决。