理论上是可以的,但只适用于全局寄存器固定的情况。
(当然,前提是ISA本身具有内存映射CPU寄存器;通常只有微控制器ISA才是这样的;这使得高性能实现更加困难。)
指针在传递给像qsort
或printf
这样的函数时必须保持有效(继续指向相同的对象),或者传递给您自己的函数。但是,复杂的函数通常会将一些寄存器保存到内存中(通常是堆栈),以在函数结束时恢复, 并在该函数内部将它们自己的值放入这些寄存器中。
因此,如果您选择普通的调用保留寄存器,则当该函数解引用您传递给它的指针时,指向CPU寄存器的指针将指向其他内容,可能是函数的某个局部变量。
我认为解决这个问题的唯一方法是为特定的C++对象在整个程序范围内保留一个寄存器。类似于GNU C/C++中的
register char foo asm("r16");
,但在假想编译器中,这并不会阻止您获取其地址。这样的假想编译器必须比GCC更严格,以确保全局变量的值始终在该寄存器中,以便通过指针进行的每个内存访问都能够实现。这与
GCC文档中对于寄存器asm全局变量的说明不同。您需要重新编译库以确保它们不使用该寄存器进行任何操作(例如
gcc -ffixed-r16
或让它们查看定义)。
当然,C++实现可以自行决定为某些C++对象(可能是全局对象)执行所有这些操作,包括生成所有库代码以遵守整个程序的寄存器分配。
如果我们只讨论在有限范围内进行此操作(而不是针对未知函数的调用),那么,如果
逃逸分析证明所有使用
p
的地方都是有限的,将
int *p = &x;
编译为获取CPU寄存器
x
的地址是安全的。我原本想说这样做没有意义,因为任何这样的证明都会给你足够的信息来优化间接性,并将
*p
编译为访问寄存器而不是内存,但是有一个用例:
如果你有两个或更多的变量,在解引用p之前执行if(condition)p =&y;编译器可能知道当评估*p时,x肯定仍然在同一个寄存器中,但不知道p是否指向x还是y。因此,保留x或y在寄存器中可能是有用的,特别是如果它们也被其他与p的解引用混合读/写的代码直接读/写。
当然,我一直假设使用的是“正常”的ISA和“正常”的调用约定。可以想象出奇怪和美妙的机器,以及在它们上面或正常机器上实现的C++,可能会有非常不同的工作方式。
ISO C++对此的看法:不多
ISO C++抽象机器仅具有内存,每个对象都有一个地址。(如果从未使用该地址,则受as-if规则的限制。)将数据加载到寄存器中是实现细节。
因此,在像AVR(8位RISC微控制器)或8051这样的机器上,C++指针可以指向它们的某些CPU寄存器1。在一些微控制器(如AVR2)上,具有内存映射的CPU寄存器是一种事物。例如,AVR微控制器中将寄存器作为内存的好处是什么?有一个图表。(并询问为什么我们需要寄存器,而不是只使用内存地址,如果它们将被内存映射。)
这个AVR Godbolt 链接并没有展示太多内容,主要只是在玩弄 GNU C 寄存器-汇编全局变量。
注1:在普通的C++实现中,对于普通的ISA,C++指针可以直接映射到一个机器地址,可以通过汇编语言进行间接引用。(在像6502这样的机器上可能非常不方便, 但仍然可以)。
在没有虚拟内存的机器上,这样一个指针通常是一个物理地址。(假设使用普通的平面内存模型,而不是分段)。我不知道有任何带有虚拟内存和内存映射CPU寄存器的ISA,但是有很多我不知道的晦涩的ISA。如果存在这样的ISA,将寄存器映射到虚拟地址空间的固定部分可能是有意义的,以便可以并行地检查地址以进行寄存器访问和TLB查找。无论哪种方式,都会使ISA的流水线实现变得非常困难,因为检测需要绕过转发(或暂停)的
RAW hazards等冲突现在涉及到检查内存访问。普通的ISA只需要在解码机器指令时相互匹配寄存器号码。随着内存允许通过寄存器进行间接寻址,
memory disambiguation / store forwarding需要与检测指令何时读取先前寄存器写入的结果进行交互,因为该读取或写入可能是通过内存进行的。
有一些旧的非流水线CPU带有虚拟内存,但是流水线是你永远不会想要在现代ISA上将寄存器内存映射为主CPU用于桌面/笔记本电脑/移动设备等性能相关领域的主要原因之一。现在,包含虚拟内存的复杂性却没有流水线设计几乎没有意义。有一些带有虚拟内存的流水线微控制器/低端CPU。
注2:在现代主流32位和64位ISA上基本不存在内存映射CPU寄存器。通用寄存器通常是否被内存映射?
带有内存映射CPU寄存器的微控制器通常将寄存器文件实现为内部SRAM的一部分,它们以任何方式作为常规内存。
在ARM、x86-64、MIPS和RISC-V以及所有类似的ISA中,寻址寄存器的唯一方式是通过将寄存器编号编码到指令的机器代码中。只有自修改代码才可能进行寄存器间接寻址,而C++并不需要这样做,正常实现也不使用这种技术。此外,寄存器编号是一个与内存分开的单独地址空间。例如,ARM有16个基本整数寄存器,因此像
add r0, r1, r2
这样的指令将在该机器指令的编码中有三个4位字段,每个操作数一个。(在ARM模式下,不是Thumb模式。)这些寄存器编号与内存地址
0
、
1
或
2
没有任何关系。
请注意,
内存映射的I/O寄存器在所有现代ISA上都很常见,通常与RAM共享物理地址空间。I/O地址通常被称为寄存器,但寄存器位于外设中,例如网络卡,而不是CPU中。读取或写入它将产生一些副作用,因此在C++中,您通常会使用
volatile int *constexpr ioport = 0x1234;
或类似的内容进行MMIO。MMIO寄存器绝对不是可以在像AArch64
add w0, w1, w2
这样的指令中使用的通用整数寄存器之一。
A
)和索引寄存器(Y
和X
)以及其他寄存器并不是。我记得没有这样的情况,现在也找不到任何相关信息。 - Some programmer dude