假设一个C++编译器为一个CPU寄存器没有内存映射的体系结构编译了代码。我们还假设同一个编译器为CPU寄存器保留了一些指针值。
例如,如果编译器出于任何原因(例如优化原因)为变量使用寄存器分配(而不是谈论寄存器关键字),并且我们打印对该变量的引用的值,则编译器将返回一个保留的";地址值";。
该编译器是否符合标准?
根据我所能收集到的信息(我还没有读过整件事——工作草案,编程语言C++标准),我怀疑该标准没有提到RAM内存或操作内存,而是定义了自己的内存模型,指针表示地址(可能是错误的)。
既然寄存器也是内存的一种形式,我可以想象,将寄存器视为内存模型一部分的实现可能是合法的。
指针指向C++寄存器合法吗?
是。
该编译器是否符合标准?
当然。
C++不知道";寄存器";,指针指向对象(和函数),而不是";存储位置";。该标准描述了程序的行为,而不是如何实现它。描述行为使它变得抽象-以什么方式和如何使用什么无关紧要,只有结果才是重要的。如果程序的行为与标准所说的相匹配,那么对象存储在哪里就无关紧要了。
我可以提到简介。记忆:
- 内存位置要么是不是位字段的标量类型对象,要么是宽度均为非零的相邻位字段的最大序列
和compound:
复合类型可以通过以下方式构建:
指向给定类型的cv void或对象或函数(包括类的静态成员)的
- 指针
[…]指针类型的每个值都是以下值之一:
- 指向对象或函数的指针(据说指针指向该对象或函数),或
- 经过对象末尾的指针([expr.add]),或
- 该类型的空指针值,或者
- 无效的指针值
[…]指针类型的值表示是由实现定义的。[…]
要对指针执行任何有用的操作,如应用*
运算符unary.op或比较指针expr.eq,它们必须指向某个对象(边缘情况除外,如比较时的NULL
)。";其中";对象的确切存储是相当模糊的——内存存储";对象";,内存本身可以在任何地方。
例如,如果编译器出于任何原因(例如优化原因)使用变量的寄存器分配(而不是谈论寄存器关键字),我们打印对该变量的引用的值,则编译器将返回一个保留的";地址值";
std::ostream::operator<<
调用std::num_put
,void*
的转换为%p
facet.num.put.virtuals。来自C99 fprintf:
[转换%]p
该论点应是指向无效的指针。指针的值以实现定义的方式转换为打印字符序列。
但请注意,从C99 fscanf:
[指定的转换%]p
匹配实现定义的序列集,该序列集应与%p可能生成的序列集相同fprintf函数的转换。相应的论点应是指向void的指针。输入项转换为指针值。如果输入项是在同一程序执行期间较早转换的值结果应与该值进行比较的指针;否则%p转换的行为未定义。
打印的内容必须是该对象唯一的,仅此而已。因此,编译器必须为寄存器中的地址选择一些唯一的值,并在请求转换时打印它们。从/到uintptr_t
的转换也必须以实现定义的方式来实现。但这一切都在实现中——代码行为如何实现的实现细节对C++程序员来说是看不见的。
指针指向C++寄存器合法吗?
是和否。在C++中,如果不反对使用register
关键字,则它是对编译器的建议,而不是要求。
编译器是否实现指向寄存器的指针取决于平台是否支持指向寄存器的指针,或者寄存器是内存映射的。有些平台的一些寄存器是内存映射的。
当编译器遇到POD变量声明时,允许编译器使用变量的寄存器。但是,如果平台不支持指向寄存器的指针,编译器可能会在内存中分配变量;尤其是当取变量的地址时。
举个例子:
int a; // Can be represented using a register.
int b;
int *p_b = &b; // The "b" variable may no longer reside in a register
// if the platform doesn't support pointers to registers.
在许多常见的平台中,例如ARM处理器,寄存器位于处理器的内存区域(一个特殊区域)内。这些寄存器没有来自处理器的地址线或数据线。因此,它们不会占用处理器地址空间中的任何空间。也没有返回寄存器地址的ARM指令。因此,对于ARM处理器,如果代码使用变量的地址,编译器会将变量的分配从寄存器更改为内存(处理器外部)。
在大多数CPU具有内存映射寄存器的情况下,使用其中一些寄存器的编译器会指定它们使用的寄存器。编译器文档中说它不使用的寄存器可以使用volatile
限定的指针来访问,就像任何其他类型的I/O寄存器一样,前提是它们不会以编译器意想不到的方式影响CPU状态。对编译器可能使用的寄存器的读取通常会产生编译器生成的代码碰巧留在那里的任何值,这不太可能有意义。编译器使用的寄存器的写入可能会以无法有效预测的方式扰乱程序行为。
理论上是的,但只对永久固定到该寄存器的全局有效
(当然,假设ISA首先带有内存映射的CPU寄存器1;通常只有微控制器ISA是这样的;这会使高性能实现变得更加困难。)
当您将指针传递给qsort
或printf
等函数或您自己的函数时,指针必须保持有效(保持指向同一对象)。但复杂的函数通常会将一些寄存器保存到内存(通常是堆栈)中,以便在函数结束时恢复,并且在该函数内部会将它们自己的值放入这些寄存器中。
因此,如果您只选择一个正常的保留调用的寄存器,当该函数取消引用您传递的指针时,指向CPU寄存器的指针将指向其他东西,可能是函数的局部变量之一。
我认为解决这个问题的唯一方法是在程序范围内为特定的C++对象保留一个寄存器。类似于全局范围内的GNU C/C++register char foo asm("r16");
,但有一个假设的编译器,其中不会阻止您获取其地址。这样一个假设的编译器必须比GCC更严格,以确保每次通过指针访问内存时,全局值总是在该寄存器中,这与GCC为寄存器asm-globals编写的文档不同。您必须重新编译库才能不将该寄存器用于任何事情(如gcc -ffixed-r16
或让它们查看定义)
当然,C++实现可以决定为某个C++对象(可能是全局对象)单独执行所有这些操作,包括生成所有库代码以遵守整个程序寄存器分配。
如果我们只讨论在有限的范围内执行此操作(不适用于对未知函数的调用),那么如果转义分析证明p
的所有使用都是有限的,那么编译int *p = &x;
以获取x
当前所在的CPU寄存器的地址是安全的。我想说这是无用的,因为任何这样的证明都会给你足够的信息来优化间接性,并编译*p
作为寄存器而不是内存进行访问,但有一个用例:
如果有两个或更多变量,并且在取消引用p
之前执行if (condition) p = &y;
,则编译器可能知道*p
求值时x
肯定仍在同一寄存器中,但不知道p
是指向x
还是指向y
。因此,将x
或y
保留在寄存器中可能很有用,特别是如果它们也由其他代码直接读取/写入,这些代码与p
的取消引用混合在一起。
当然;正常的";ISA和一个";正常的";呼叫约定。可以想象奇怪而奇妙的机器,和/或它们或普通机器上的C++实现,它们的工作方式可能会大不相同。
ISO C++对此有何看法:不多
ISO C++抽象机only具有内存,并且每个对象都有一个地址。(如果从未使用过地址,则受"好像"规则的约束。)将数据加载到寄存器是一个实现细节。
因此,是的,在像AVR(8位RISC微控制器)或8051这样的机器中,一些CPU寄存器是内存映射的,C++指针可以指向它们1。在AVR2这样的一些微控制器上,拥有内存映射的CPU寄存器是一件事。(例如,在AVR微控制器中,将寄存器作为内存的一部分有什么好处?有一个图表
这个AVR Godbolt链接并没有真正显示太多,主要只是在玩GNUC寄存器asm-global。
脚注1:在正常ISAs的正常C++实现中,C++指针非常直接地映射到可以以某种方式从asm取消引用的机器地址。(也许在6502这样的机器上很不方便,但仍然如此)。
在没有虚拟内存的机器中,这样的指针通常是一个物理地址。(假设是一个普通的平面内存模型,而不是分段的。)我不知道有任何带有虚拟内存和内存映射CPU寄存器的ISAs,但有很多我不知道的模糊ISAs。如果存在,那么将寄存器映射到虚拟地址空间的固定部分可能是有意义的,这样可以在TLB查找的同时检查地址的寄存器访问。无论哪种方式,它都会使ISA的流水线实现变得非常痛苦,因为检测像RAW这样需要旁路转发(或停滞)的危险现在需要检查内存访问。在解码机器指令时,普通的ISAs只需要将寄存器号相互匹配。由于内存允许通过寄存器进行间接寻址,内存消除歧义/存储转发将需要与检测指令何时读取上一次寄存器写入的结果进行交互,因为该读取或写入可以通过内存进行。
有一些旧的带有虚拟内存的非流水线CPU,但流水线是一个主要原因,你永远不希望内存映射到现代ISA上的寄存器,并有任何野心将其用作与性能相关的台式机/笔记本电脑/移动设备的主CPU。如今,将虚拟内存的复杂性包括在内是没有意义的,但而不是流水线设计。有一些流水线微控制器/低端CPU没有虚拟内存。
脚注2:内存映射的CPU寄存器在现代主流的32位和64位ISAs上基本上不存在。通用寄存器通常是内存映射的吗?
具有内存映射CPU寄存器的微控制器通常将寄存器文件作为内部SRAM的一部分来实现,它们无论如何都必须充当常规内存。
在ARM、x86-64、MIPS和RISC-V以及所有类似的ISA中,寻址寄存器的唯一方法是将寄存器编号编码到指令的机器代码中。寄存器间接寻址只有在具有自修改代码的情况下才可能实现,否则C++不需要这些代码,而正常实现也不使用这些代码。此外,寄存器号是一个独立于内存的地址空间。例如,ARM有16个基本整数regs,因此像add r0, r1, r2
这样的指令在该机器指令的编码中将有三个4位字段,每个操作数一个。(在ARM模式下,不是Thumb。)这些寄存器号与存储器地址0
、1
或2
无关。
请注意,内存映射的I/O寄存器在所有现代ISA上都很常见,通常与RAM共享物理地址空间。I/O地址通常被称为寄存器,但寄存器在外围设备中,就像网卡一样,而不是在CPU中。读或写它会有一些副作用,所以在C++中,你通常会为MMIO使用volatile int *constexpr ioport = 0x1234;
或其他东西。MMIO寄存器绝对不是像AArch64add w0, w1, w2
这样的指令中可以使用的通用整数寄存器。