你是一家餐厅的主厨,而编译器是你的帮厨。绝大多数菜你可以放手让帮厨去做,但有几道“私房菜”必须你亲自上灶——这就是内联汇编。问题来了:你亲自炒菜时,需要明确告诉帮厨怎么配合:用哪个灶台(寄存器)、从哪儿取食材(内存)、哪些调料按固定量放(立即数)、哪口锅别人不能碰(& 早期clobber)、冰箱动过之后要重新清点(memory clobber)、用同一个碗装料和成品(匹配约束)。 这些“指令备注”就是 GCC 内联汇编的约束字符串。它不参与菜品本身的味道,却决定了后厨的运转效率与是否翻车。本文将带你从零认识这套“点菜暗号”,看懂它、会用它、能避开它的坑。

岗位关联

写内核模块、驱动或高性能中间件时,内联汇编是绕不开的“最后一公里”。掌握约束系统,意味着你能安全地让编译器替你管理寄存器与内存,而不是靠猜——这是系统级开发者必备的底层素养。

目录
  1. 可视化演示
  2. 约束字符串结构
  3. 寄存器约束
  4. 内存约束
  5. 立即数约束
  6. 早期clobber
  7. 内存屏障
  8. 匹配约束
  9. 约束组合实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示约束类型映射、操作数分配流程与clobber声明机制,支持寄存器分配策略调节。

本节配有交互动效(进入视口后自动加载,约 3KB)。

约束类型映射 紫色r约束将操作数分配到通用寄存器 黄色m约束将操作数分配到内存地址 r → reg | m → mem | i → imm 约束字符串决定操作数的存储位置

约束字符串结构

想象你是一位主厨,今天要亲自做一道“私房葱油面”。你递给帮厨的订单上写着:“面条现煮,葱油现熬,不要香菜”。这里的“不要香菜”就是一种约束——它限制了帮厨的操作范围。GCC 内联汇编的约束字符串,就是你在汇编指令旁写给编译器的“订单备注”。

一条完整的内联汇编语句,包含四个用冒号分隔的区域:指令模板、输出操作数、输入操作数、clobber 列表。编译器看到这段代码,会把你指定的约束当作“硬性要求”,在寄存器分配时严格执行。

这个“四段式”结构是内联汇编的地基。就像订单上的每个分区都有固定含义,编译器按区域解析你的意图。如果少写一个冒号,编译器就会把输入操作数当作输出,轻则结果错误,重则整个程序崩溃——这正是许多初学者在“点菜”时常犯的乌龙。

asm("指令模板"  :  "输出操作数"  :  "输入操作数"  :  "clobber列表")\text{asm}\left(\text{"指令模板"}\; :\; \text{"输出操作数"}\; :\; \text{"输入操作数"}\; :\; \text{"clobber列表"}\right)
这个公式展示了内联汇编的“四段式”骨架,前三段是操作数区,第四段是破坏清单。
  1. asm 关键字告诉编译器“这段是内联汇编,请按汇编规则处理”。
    asm(⋯ )\text{asm}(\cdots)
  2. 指令模板以字符串形式书写汇编指令,其中用 %0、%1 等占位符引用后面的操作数。
    "movl %1,%0"\text{"movl } \%1, \%0\text{"}
  3. 输出操作数区第一个冒号后,列出需要“写回”C变量的操作数,通常带 = 修饰符。
    ":" "=r"(result)\text{":" } \text{"=r"}(result)
  4. 输入操作数区第二个冒号后,列出只读输入,约束如 "r"(x)、"m"(y)。
    ":" "r"(x)\text{":" } \text{"r"}(x)
  5. clobber 列表第三个冒号后,声明指令会破坏哪些资源(如 "cc" 标志位、"memory" 内存)。
    ":" "memory"\text{":" } \text{"memory"}

寄存器约束

帮厨手边有一排调料盒——盐、糖、酱油。你喊“快拿盐!”,他不会去仓库翻,而是随手从离灶台最近的调料盒里抓。寄存器约束 r 就是这个意思:告诉编译器,这个操作数请放进任意一个通用寄存器,具体哪个由编译器决定。

寄存器是 CPU 内部最快的存储位置,但数量稀少(通常十几个)。"r" 约束让编译器掌握分配自由——像帮厨一样,知道哪个调料盒最顺手就用哪个。对应到 C 语言变量,输出写作 "=r"(var),输入写作 "r"(x),读写一体写作 "+r"(var)。

值得注意:r 约束不指定具体寄存器,而是把选择权交给编译器。好比客户说“来杯热饮”,至于红茶还是咖啡,由递餐的人决定。这种灵活性让编译器能更好地做全局优化——如果你硬写成 %eax,反而限制了编译器的调度空间,容易引入不必要的寄存器冲突。

"=r"(v)  ⇒  v∈R通用寄存器\text{"=r"}(v) \;\Rightarrow\; v \in R_{\text{通用寄存器}}
= 表示输出(写入),r 表示寄存器,合起来就是“结果写回一个通用寄存器指向的变量”。
  1. 约束修饰符 =声明这是个输出操作数,指令执行后会把寄存器值写回 v。
    "="\text{"="}
  2. 约束字母 r允许编译器从通用寄存器池中任选一个,如 %eax、%ebx 等。
    r∈{eax, ebx, ecx, edx, ...}r \in \{\text{eax, ebx, ecx, edx, ...}\}
  3. 蕴含关系\Rightarrow 表示“推导出”,即满足约束后变量 v 一定驻留在寄存器中。
    v∈Rv \in R
  4. 操作数展开汇编模板中通过 %0 引用这个寄存器,编译器会替换成真实的寄存器名。
    "movl $5,%0"\text{"movl } \$5, \%0\text{"}

内存约束

如果食材量很大——比如一筐土豆,你不会全搬到灶台上,而是放在储藏室,需要时去取。内存约束 m 就是:“这个操作数放在内存中,直接对内存做操作,别往寄存器搬。”

为什么需要它?当操作数是大型结构体、数组元素或某个被反复读写的共享变量时,把它塞进寄存器可能得不偿失——搬进搬出反而更慢。m 约束让编译器生成直接访存的指令(如 movl 带内存寻址),操作数保持在原来的内存地址上。

使用 m 约束时,通常配合内存屏障或 volatile 使用,防止编译器把多次访存优化成一次。就像冰箱里的食材,你不希望帮厨凭记忆“以为”还剩多少——每次取用都得亲自打开冰箱确认。在多媒体处理、设备驱动等场景,m 约束能减少寄存器压力,代价是访问速度慢一两个数量级,这正是“容量换速度”的经典权衡。

"m"(v)  ⇒  v∈M内存单元\text{"m"}(v) \;\Rightarrow\; v \in M_{\text{内存单元}}
含义简洁:m 约束的操作数驻留内存,而非寄存器。
  1. 约束字母 m声明操作数在内存中,编译器无需分配寄存器给它。
    "m"\text{"m"}
  2. 变量 vv 是 C 语言中的左值(可寻址变量),编译器会取其地址参与汇编。
    v∈Mv \in M
  3. 内存地址表达式在汇编模板中,%0 展开为内存操作数,如 (%ebp)、8(%esp) 等寻址形式。
    "movl %1,%0"⇒(memory operand)\text{"movl } \%1, \%0\text{"} \Rightarrow \text{(memory operand)}

立即数约束

菜谱上写着“加 3 克盐”——你不需要去任何地方取盐,因为数字就写在纸条上。立即数约束 i 同理:操作数是一个编译期就知道的整数常量,直接嵌入指令中,不占用寄存器也不占内存。

在约束字符串中,"i"(const) 要求 const 必须是编译时常量:字面量、#define 宏、enum 值、sizeof 表达式都行,但不能是运行时变量。

立即数约束最大的价值是“零成本”——它不占寄存器、不访内存、不消耗额外指令周期。典型应用是位移量、掩码、循环边界等常量。常见误区是把 i 用在了运行时变量上,编译器会直接报错“无法编译”。简单说:写菜谱时数字是死的,炒菜时它也是死的,i 就是这种“死的数字”。

"i"(c)  ⇒  c∈Z编译期常量\text{"i"}(c) \;\Rightarrow\; c \in \mathbb{Z}_{\text{编译期常量}}
核心含义:i 约束操作数在编译阶段就已确定,不会被分配任何寄存器或内存空间。
  1. 约束字母 i表示立即数(immediate),即直接编码在指令中的常量。
    "i"\text{"i"}
  2. 常量取值范围c 必须是整数常量,范围受指令格式限制(如 x86 的 addl $imm, %reg 通常 32 位)。
    c∈Zc \in \mathbb{Z}
  3. C 变量绑定写为 "i"(MAX_SIZE),编译器将 MAX_SIZE 的值直接填进指令的立即数字段。
    "movl $MAX_SIZE,%0"\text{"movl } \$MAX\_SIZE, \%0\text{"}
text1 行
movl $42, %eax   ; $42 就是一个立即数

早期clobber

餐厅有一口“三号锅”,平时大家共用。但你要做一道“抹茶千层”,要求这口锅从开始备料到最后装盘,全程只属于你一个人,不许帮厨拿它焯青菜——否则串味。早期 clobber 约束 & 就是这个意思:这个输出操作数在指令一开始就会被写入,因此绝不能与任何输入操作数共用寄存器。

为什么需要?考虑一条乘加指令:它先读输入,再写输出。如果编译器把输出安排到了某个输入所在的寄存器,而指令的执行顺序是先覆盖再读取,输入就被提前破坏了。

说人话:& 是“独占宣言”。编译器的寄存器分配器看到 &,会为输出划出一个不属于任何输入的“隔离区”。在乘法、除法等“边读边写”的指令中,这个约束是防止数据错乱的保险栓。省略它,编译器的优化可能让你在 Debug 版跑得欢、Release 版跑得崩——因为 -O2 下它才敢大胆复用寄存器。

O∩Ik=∅(∀k)O \cap I_k = \varnothing \quad (\forall k)
该公式表达:输出操作数集合与任一输入操作数集合的交集为空——& 强制它们物理隔离。
  1. & 修饰符写在约束最前面,如 "=&r"(out),表示输出是“早期写入”。
    "=&r"\text{"=\&r"}
  2. 输出集合 O指令写入的寄存器集合,& 要求它在指令开始时就必须独立。
    O={rout}O = \{r_{out}\}
  3. 输入集合 I_k第 k 个输入操作数占用的寄存器集合。
    Ik={rin(k)}I_k = \{r_{in}^{(k)}\}
  4. 交集为空\cap = \varnothing 保证输出与所有输入寄存器都不重叠。
    O∩Ik=∅O \cap I_k = \varnothing

内存屏障

你把冰箱翻了个底朝天——从冷藏室取出冻肉,从冷冻室拿出冰激凌,又把蔬菜挪了个位置。这时你告诉帮厨:“冰箱里东西全变样了,你之前记的库存单作废,下次要用什么,必须重新开冰箱确认。”内存屏障关键字 memory 也是这个逻辑:告诉编译器,这段汇编代码会修改内存中的内容,编译器必须放弃之前对内存值的缓存假设,重新加载所有相关变量。

在 clobber 列表末位写上 "memory",就是给编译器的一道“强制刷新”指令:

没有 "memory" 时,编译器可能把某个变量缓存进寄存器,内联汇编改了内存,编译器却还傻乎乎地用旧寄存器值——典型的“帮厨记错了库存”。尤其多线程或驱动代码,asm volatile(... ::: "memory") 是确保内存视图一致性的常用手段。代价是性能:编译器不能再把变量留在寄存器里,每次使用都访存。它像一道“安全闸”:开销不小,但能挡住大灾。

∀v∈M编译器已知:  重新加载(v)\forall v \in M_{\text{编译器已知}}:\; \text{重新加载}(v)
该公式表示:对于编译器原本缓存的所有内存变量,在执行完内联汇编之后,都必须从内存重新读取,不得沿用旧值。
  1. memory 关键字专门出现在 clobber 区(第四个冒号后),声明“内存被我改了”。
    "memory"\text{"memory"}
  2. 全体内存变量 v\forall v \in M 涵盖编译器之前跟踪的所有变量,不区分类型。
    v∈M已知变量v \in M_{\text{已知变量}}
  3. 强制重载\text{重新加载}(v) 意味着寄存器缓存失效,后续访问必须重新访存。
    重新加载(v)\text{重新加载}(v)

匹配约束

“就用刚才那个碗!”——装料和装成品用同一个容器,省得洗两个碗。匹配约束就是这个思路:让输入操作数与之前的某个输出操作数共用同一寄存器(或内存位置)。写法非常直白:约束字符串写 "0",表示“与第 0 号操作数共用一个位置”。

这在“就地更新”类操作中非常常见——比如计算 x = x + y,输出恰好复用了输入的寄存器,既省寄存器又省指令。

匹配约束最大的好处:避免无意义的寄存器搬运。但要注意,与 & 配合时的语义——如果输出带 &,匹配输入也同样不允许被其他输入“染指”。很多新手把 "0" 当成普通输入约束,结果编译器好心分配了新寄存器,语义瞬间崩塌。记住:匹配匹配,配对成双——它和 & 一样,是对“位置”的强约束。

O0≡I1(同一存储位置)O_0 \equiv I_1 \quad (\text{同一存储位置})
公式含义:第 1 个输入操作数(约束为 "0")与第 0 个输出操作数占用完全相同的寄存器或内存位置。
  1. 数字 0 的语义约束 "0" 不是“立即数0”,而是对第 0 个操作数的引用。
    "0"≡操作数#0\text{"0"} \equiv \text{操作数} \#0
  2. 等价关系\equiv 表示两者共享同一物理存储位置,编译器必须将它们映射到同一个寄存器或地址。
    O0≡I1O_0 \equiv I_1
  3. 输入绑定输出在指令模板中,%1 和 %0 会被替换成同一个寄存器名。
    "addl %1,%0"⇒addl %eax,%eax\text{"addl } \%1, \%0\text{"} \Rightarrow \text{addl } \%eax, \%eax

约束组合实践

现在已经集齐了 r、m、i、&、memory、匹配约束,最后要讲的是“怎么串起来用”。就像一桌酒席,每道菜有专属做法,但整体搭配才见功力。约束组合的核心技巧有三个:备选约束、volatile 修饰、调试技巧。

备选约束写作 "r,m"——用逗号分隔,告诉编译器“这两种都行,你挑方便的那种”。比如有些寄存器紧张时,编译器可以选择把操作数放内存:

工程实践中还有几条“黄金法则”:一是用 asm volatile 防止编译器把汇编块重排或删除,尤其是带副作用的指令;二是用 named operands(%[name] 命名操作数)让模板更可读;三是调试时让编译器生成可读汇编(-S 选项),亲眼验证约束是否按预期生效。约束是写给你的帮厨(编译器)看的,而帮厨是否听话,必须打开汇编代码来驗收。

"r,m"  ≡  {R,  M}\text{"r,m"} \;\equiv\; \{R,\; M\}
公式说明:带逗号的约束字符串表示一个“备选集合”,编译器按顺序尝试,优先满足靠前者。
  1. 逗号分隔"r,m" 表示“寄存器或内存皆可”,这是一个集合。
    {R,M}\{R, M\}
  2. 等价关系\equiv 表示整个约束字符串等价于一个可选集合,编译器从集合中挑选最有利的分配。
    "r,m"≡{R,  M}\text{"r,m"} \equiv \{R,\; M\}
  3. 分配策略编译器优先尝试 r(寄存器),若寄存器不足或优化更优,则回退到 m(内存)。
    尝试 R→回退 M\text{尝试 } R \rightarrow \text{回退 } M

AI 应用场景

Linux内核中的内存屏障与原子操作

阶段:底层系统原语实现

解决的问题:多核 CPU 存在缓存一致性延迟,内核需要保证内存操作顺序。传统写法靠经验加锁容易出错,利用 asm volatile("" ::: "memory") 可强制编译器生成内存屏障指令,从而阻断错误的重排优化。

具体案例:Linux 内核的 smp_mb() 宏本质上就是内联汇编 + memory clobber。当驱动开发者编写自旋锁或 RCU 时,编译器不会擅自把变量缓存进寄存器,确保多核间的可见性。

高性能计算中的SIMD与访存优化

阶段:算子的底层手工调优

解决的问题:AI 推理框架的卷积或矩阵乘算子,在关键热区用内联汇编手写向量指令。r 约束让编译器自动分配寄存器,m 约束让大数据块留在缓存友好的内存中,& 约束防止中间结果覆盖输入特征图——这些都是向量算子正确性的保障。

具体案例:某推理引擎的 int8 GEMM 算子,使用内联汇编加载 %ymm 寄存器,输出部分用 "=&r" 声明独占。若漏掉 &,Release 版推理精度偶发跳变,排查三天才发现是寄存器复用导致输入被提前覆盖。

嵌入式设备寄存器读写

阶段:硬件抽象层(HAL)开发

解决的问题:操作外设寄存器(如 UART、GPIO)时,既要保证指令顺序,又要避免编译器把寄存器访问优化掉。volatile 加 memory clobber 是标配,让每一次读写都真实落在硬件寄存器上。

具体案例:一款 MCU 上的传感器驱动,通过内联汇编实现“读-改-写”控制寄存器。约束用 "+r"(reg) 实现就地更新,配合 "memory" 确保关闭中断期间的操作不被优化乱序,现场问题率下降 90%。

Python 演示

python46 行
class GccConstraintSim:    """模拟 GCC 内联汇编约束的寄存器分配过程"""     def __init__(self, reg_count=4):        self.regs = [f'%r{i}' for i in range(reg_count)]        self.used_set = set()        self.lock_set = set()     def pick_reg(self, exclusive):        for r in self.regs:            if r not in self.used_set and not (exclusive and r in self.lock_set):                self.used_set.add(r)                if exclusive:                    self.lock_set.add(r)                return r        raise RuntimeError('寄存器耗尽')     def alloc(self, cons):        if cons.isdigit():            return f'复用操作数{cons}的位置'        early = '&' in cons        kind = 'm' if 'm' in cons else ('i' if 'i' in cons else 'r')        if kind == 'r':            reg = self.pick_reg(early)            return f'寄存器{reg}{"(独占)" if early else ""}'        if kind == 'm':            return '内存单元'        if kind == 'i':            return '立即数'        return '未知'     def match(self, idx):        return f'复用操作数{idx}的位置'     def run(self, constraints):        for i, c in enumerate(constraints):            print(f'操作数{i}: 约束[{c}] -> {self.alloc(c)}')  def main():    sim = GccConstraintSim(4)    test_constraints = ['=r', 'r', 'm', 'i', '=&r', '0']    sim.run(test_constraints)  main()
目的:用 Python 模拟编译器收到约束字符串后的“资源分配”行为,直观展示 r/m/i/&/匹配约束各自的效果。
  1. self.used_set = set()记录已分配资源,lock_set 记录被 & 独占的寄存器,二者共同决定下一次分配的候选集合。
  2. def alloc(self, cons)核心分配函数,先判断数字匹配,再识别 &,最后按 r/m/i 分类处理。
  3. early = '&' in cons检测早期 clobber 标记,若存在则将独占标志传给 pick_reg。
  4. if kind == 'r': reg = self.pick_reg(early)寄存器约束走独立分配路径,early 为真时禁止复用。
  5. if cons.isdigit(): return f'复用操作数{cons}的位置'匹配约束直接返回对已有位置的引用,不新增资源。
  6. test_constraints = ['=r', 'r', 'm', 'i', '=&r', '0']覆盖六种典型约束的一轮测试,输出可以直观对照正文各节。

常见误区

误区我写 `"=r"(var)` 是不是就是让编译器把结果放到名字叫 `r` 的寄存器里?那我取结果时是不是要去读 `%r`?

正解"=r" 是“约束修饰符 =+ 约束字母 r”的组合,r 表示“任意通用寄存器”,不是寄存器名。具体是 eax 还是 ebx,由编译器分配,你在汇编模板里用 %0 引用即可。就像你说“拿个碗来”,帮厨递给你圆的还是方的,你直接用就行。

为何常见:初学汇编的人习惯用具体寄存器名(eax、ebx)思考,而内联汇编的设计哲学恰恰是“让编译器管事”,抽象层级不一样。

误区`asm("addl $1, %0" : "=r"(x))` 不写 clobber 也能跑,所以 clobber 列表空着没事吧?

正解clobber 列表是“危险声明”。如果指令会改动标志寄存器、内存或任何未列出的资源,不写 clobber 就是欺骗编译器——它以为这些资源没事,继续缓存旧值,轻则结果错误,重则程序崩溃。同理,volatile 缺失也可能让编译器把整个汇编块优化掉。

为何常见:小例子在低优化级别下都能跑通,掩盖了 clobber 缺失的隐患。等开了 -O2 才暴雷,但那时代码已经在生产环境了。

误区`asm("..." : "=r"(out) : "0"(in))` 里的 `"0"` 难道是立即数 0?那指令执行时会不会把常数值 0 传给汇编?

正解"0" 是匹配约束,表示“和第 0 号操作数(即 out)用同一个寄存器或内存位置”。它告诉编译器:in 和 out 共享资源。在模板里 %1 和 %0 会展开成同一个寄存器名,实现就地更新,跟“0 这个数”没有半点关系。

为何常见:看到数字 0 就联想到“零值”,是字面联想在作祟。理解操作数编号(%0、%1)后,自然明白 "0" 指向的是“第 0 个操作数”而非数值。

误区为了“保险”,不管指令是否改了内存,都在 clobber 末尾加 `"memory"`,认为反正不会错。

正解memory clobber 会让编译器丢弃所有已知的内存变量缓存,后续每个变量访问都可能重新访存——性能代价极高。在热循环里滥用,可能把原本几纳秒的操作拖到几十纳秒。应该精确声明实际破坏的资源,能用具体的变量约束就别用全量内存屏障。

为何常见:“加一个参数不会致命”的心理在约束场景下失效——memory 的破坏面是整个内存视图,代价是全局性的。

面试题

Q1. GCC 内联汇编的四个部分之间用什么符号分隔?

单选题 · easy

  • A.分号
  • B.冒号← 正确答案
  • C.逗号
  • D.竖线

答案:B。内联汇编用三个冒号分隔“指令模板、输出、输入、clobber”四个区域。分号在 C 语言里是语句结束符,逗号只用于操作数列表内部,竖线则不参与语法。记住“四段冒号”像订单上四个栏目,各自独立。

Q2. 约束 "r" 表示操作数应当由什么来承载?

单选题 · easy

  • A.内存单元
  • B.栈顶指针
  • C.任意通用寄存器← 正确答案
  • D.立即数字段

答案:C。"r" 是寄存器约束,编译器从通用寄存器池中任选一个,操作数不直接绑定具体寄存器名。内存是 "m",立即数是 "i",栈顶指针是具体寄存器而非通用约束。就像“拿个碗”不指定是哪个碗。

Q3. 约束 "i" 适用于以下哪个操作数?

单选题 · easy

  • A.运行时局部变量
  • B.编译期整数常量← 正确答案
  • C.动态分配的结构体
  • D.浮点寄存器变量

答案:B。"i" 要求操作数是编译期常量,如字面量、宏定义或 enum,编译器会把它直接嵌入指令的立即数字段。运行时变量无法满足“编译期确定”的要求,强行使用会编译失败。就像菜谱上写死的“加 3 克盐”,不能是“根据心情加”。

Q4. "=&r" 约束中 & 的核心作用是?

单选题 · mid

  • A.强制使用 eax 寄存器
  • B.防止输出与输入共用寄存器← 正确答案
  • C.使指令自带内存屏障
  • D.声明输出可以是立即数

答案:B。& 是早期 clobber 标记,声明输出在指令一开始就写入,因此不允许与任何输入操作数同占一个寄存器。它不指定寄存器(那是具体寄存器名的事),也不带内存屏障语义。相当于“这口锅我包了,别人不许用”。

Q5. clobber 列表中写 "memory" 会告诉编译器什么?

单选题 · mid

  • A.内存空间需要重新分配
  • B.内存已被指令修改,需重载变量← 正确答案
  • C.禁止使用内存寻址
  • D.指令只读内存不写内存

答案:B。"memory" 充当内存屏障,编译器必须放弃对内联汇编之前所有内存变量的缓存,后续访问重新从内存读取。这是保证多线程/驱动代码内存视图一致性的关键。它不涉及内存分配,也不禁止寻址,恰恰相反,是声明“内存被动了”。

Q6. 匹配约束 "0" 表示什么意思?

单选题 · mid

  • A.操作数必须等于整数零
  • B.与第 0 个操作数共用位置← 正确答案
  • C.强制使用 0 号寄存器
  • D.在指令中嵌入立即数 0

答案:B。"0" 是数字匹配约束,指向操作数编号 0,让当前操作数与第 0 个操作数共享同一个寄存器或内存位置。它用于“输入绑定输出”的原地更新场景,并非数值零。就像“用刚才那个碗”,不关心碗里原来的数值。

Q7. 约束 "m" 适合用于什么场景?

单选题 · mid

  • A.需要立即参与运算的常量
  • B.大型结构体或数组的内存操作← 正确答案
  • C.必须使用特定寄存器的情况
  • D.只需要读一次的标量变量

答案:B。"m" 约束让操作数留在内存中,编译器生成访存指令直接操作它。适合结构体、数组成员等大数据块,避免“搬进寄存器再搬出去”的浪费。立即数常量是 "i",特定寄存器需具体约束名,标量变量用 r 往往更高效。

Q8. 下列哪个写法能正确避免“输出覆盖输入”的经典陷阱?

单选题 · hard

  • A.asm("..." : "=r"(out) : "0"(in))
  • B.asm("..." : "=&r"(out) : "r"(in))← 正确答案
  • C.asm("..." : "=r"(out) : "r"(in))
  • D.asm("..." : "+r"(out) : "r"(in))

答案:B。& 显式禁止输出与输入共用寄存器,是避免覆盖的常规手段。A 用匹配约束让 in 和 out 故意共用,本身是“有意共享”;C 没有 &,编译器可能顺手把 out 和 in 分配在同一寄存器,若指令先写后读则出错;D 的 +r 表示读写一体,输入和输出本来就是同一个变量,不符合“输入是独立变量”的前提。

Q9. 关于内存约束 "m",下列说法正确的是?

多选题 · hard

  • A.适合大数据结构避免搬运← 正确答案
  • B.比寄存器约束访问更慢← 正确答案
  • C.强制使用指针变量执行
  • D.可以减少寄存器分配压力← 正确答案

答案:ABD。"m" 直接操作内存,省去搬运大结构的开销(A 对);内存访问延迟比寄存器高一个数量级(B 对);它不强制用指针,只是生成内存寻址指令(C 错);因为操作数不进寄存器,自然减少了寄存器占用(D 对)。这就是“容量换速度”的权衡。

Q10. 关于约束组合与 clobber 的综合判断,哪些说法正确?

多选题 · extreme

  • A.& 与匹配约束同时使用时仍要求独占← 正确答案
  • B."r,m" 表示编译器可以在寄存器与内存之间二选一← 正确答案
  • C.memory clobber 允许编译器继续缓存旧值
  • D.asm volatile 可以防止汇编块被优化删除← 正确答案

答案:ABD。& 声明的是“输出早写不可共享”,即使走匹配约束,该位置同样不能与别的输入重叠(A 对);逗号分隔的约束表示备选集合,编译器按需挑选(B 对);memory 恰恰相反,它强制重载、禁止沿用缓存(C 错);volatile 避免编译器对汇编块做删除或重排等激进优化(D 对)。