首页 文章

现代x86硬件可以不将单个字节存储到内存中吗?

提问于
浏览
22

说到C的并发内存模型,Stroustrup的C编程语言,第4版,第1节 . 41.2.1,说:

...(像大多数现代硬件一样)机器无法加载或存储任何小于单词的东西 .

但是,我的x86处理器,几年前,可以存储小于一个字的对象 . 例如:

#include <iostream>
int main()
{
    char a =  5;
    char b = 25;
    a = b;
    std::cout << int(a) << "\n";
    return 0;
}

如果没有优化,GCC将其编译为:

[...]
        movb    $5, -1(%rbp)   # a =  5, one byte
        movb    $25, -2(%rbp)  # b = 25, one byte
        movzbl  -2(%rbp), %eax # load b, one byte, not extending the sign
        movb    %al, -1(%rbp)  # a =  b, one byte
        [...]

评论是由我提出的,但是汇编是由GCC提出的 . 当然,它运行良好 .

显然,我不明白Stroustrup在谈到硬件可以加载和存储任何小于一个单词的内容时所说的内容 . 据我所知,我的程序除了加载和存储小于单词的对象外什么都不做 .

C对零成本,硬件友好的抽象的彻底关注使C与其他易于掌握的编程语言区别开来 . 因此,如果Stroustrup在 Bus 车上有一个有趣的信号心理模型,或者有其他类似的东西,那么我想了解Stroustrup的模型 .

什么是Stroustrup谈论,拜托?

LONGER QUOTE WITH CONTEXT

这是Stroustrup在更全面的背景下的引用:

考虑如果链接器在内存中的同一个单词中分配[char类型的变量如c和b]并且(像大多数现代硬件一样)机器无法加载或存储小于单词的任何内容,可能会发生什么....明确且合理的内存模型,线程1可能会读取包含b和c的单词,更改c,并将单词写回内存 . 同时,线程2可以对b执行相同的操作 . 然后,无论哪个线程设法首先读取该单词,哪个线程设法将其结果写回内存最后将确定结果....

ADDITIONAL REMARKS

我不相信Stroustrup在讨论缓存行 . 即使他是,据我所知,缓存一致性协议将透明地处理该问题,除非在硬件I / O期间 .

我检查了处理器的硬件数据表 . 电子方面,我的处理器(Intel Ivy Bridge)似乎通过某种16位多路复用方案来解决DDR3L内存问题,所以我不知道那是什么意思 . 我不清楚这与Stroustrup的观点有很大关系 .

Stroustrup是一个聪明的人,也是一位杰出的科学家,所以我不怀疑他正在采取一些明智的做法 . 我很迷惑 .

另见this question.我的问题在几个方面类似于链接的问题,链接问题的答案在这里也很有帮助 . 然而,我的问题还在于硬件/总线模型,它促使C成为它的方式,并导致Stroustrup写出他写的东西 . 我不仅仅针对C标准正式保证的答案寻求答案,而且还希望理解为什么C标准会保证它 . 什么是潜在的想法?这也是我的问题的一部分 .

6 回答

  • 2

    不确定Stroustrup是什么意思“WORD” . 也许它是机器内存的最小尺寸?

    无论如何,并非所有机器都是使用8位(BYTE)分辨率创建的 . 事实上,我推荐Eric S. Raymond撰写的这篇精彩文章描述了计算机的一些历史:http://www.catb.org/esr/faqs/things-every-hacker-once-knew/

    “......众所周知,36位架构解释了C语言的一些不幸特性 . 最初的Unix机器PDP-7具有18位字,对应于较大的36位半字 - 位计算机 . 这些更自然地表示为六位八进制(3位)数字 . “

  • 1

    作者似乎关注线程1和线程2进入读取 - 修改 - 写入的情况(不是在软件中,软件执行两个单独的字节大小的指令,在线逻辑的某个地方必须进行读取 - modify-write)代替理想读取修改写入读取修改写入,成为读取读取修改修改写入或其他一些时序,使得读取预修改版本和最后一次写入获胜 . 读取修改修改写入写入,或读取修改读取修改写入或读取修改读取写入修改写入 .

    关注的是从0x1122开始,一个线程想要使其成为0x33XX,另一个想要使其成为0xXX44,但是例如读取读取修改修改写入写入最终得到0x1144或0x3322,但不是0x3344

    一个理智的(系统/逻辑)设计只是没有这个问题肯定不适用于像这样的通用处理器,我已经处理过像这样的时序问题的设计,但这不是我们在这里讨论的,完全不同的系统设计用于不同的目的 . 读取 - 修改 - 写入在一个理智的设计中没有跨越足够长的距离,而x86s是理智的设计 .

    读 - 修改 - 写将发生在非常接近所涉及的第一个SRAM(理想情况下,L1以典型方式运行x86,操作系统能够运行C编译的多线程程序)并且在几个时钟周期内发生,因为ram是在理想的公共汽车速度 . 正如彼得所指出的,这被认为是在缓存中体验这一点的整个缓存行,而不是处理器核心和缓存之间的读取 - 修改 - 写入 .

    即使与多核系统“同时”的概念也不一定是同时出现的,最终你会被序列化,因为性能不是基于它们从头到尾并行,而是基于保持总线加载 .

    引用是说变量分配给内存中的同一个单词,所以这是同一个程序 . 两个独立的程序不会共享这样的地址空间 . 所以

    欢迎你试试这个,做一个多线程的程序,一个写入地址0xnnn00000,另一个写入地址0xnnnn00001,每个写一个写,然后读取或更好的几个写入相同的值比一个读取,检查读取是他们写的字节,然后重复不同的值 . 让它运行一段时间,小时/天/周/月 . 看看你是否绊倒了系统...使用汇编作为实际的写入指令,以确保它正在做你所要求的(不是C或任何做或声称它的编译器不会将这些项放在同一个单词中) . 可以添加延迟以允许更多的缓存驱逐,但这会降低“同时”冲突的几率 .

    你的例子只要你确保你没有坐在边界(缓存或其他)的两边,如0xNNNNFFFFF和0xNNNN00000,隔离两个字节写入地址,如0xNNNN00000和0xNNNN00001有指令背靠背,看看你是否得到读取读取修改修改写入写入 . 围绕它进行测试,每个循环的两个值都不同,您可以在以后的任何延迟中读回整个单词,并根据需要检查这两个值 . 重复几天/几周/几个月/几年,看它是否失败 . 阅读处理器执行和微代码功能,了解它对该指令序列的作用,并根据需要创建一个不同的指令序列,试图在处理器内核远端的少数几个时钟周期内启动事务 .

    编辑

    引号的问题在于,这完全是关于语言和使用的 . “像大多数现代硬件一样”将整个主题/文本放在一个敏感的位置,它太模糊了,一方可以说我所要做的就是找到一个真实的案例,使其余的都成真,同样一方面如果我找到一个案例,其余部分都是不正确的,我可以争辩说 . 使用像那种混乱这个词作为一个可能走出监狱免费卡 .

    实际情况是,我们数据的很大一部分存储在8位宽存储器的DRAM中,只是因为我们不以8位宽访问它们,我们通常一次访问其中的8位,64位宽 . 在几周/几个月/几年/几十年内,这种说法是不正确的 .

    更大的引用说“同时”,然后说读...首先,写...最后,第一个也是最后一个,同时一起没有意义,它是并行还是串行?上下文作为一个整体关注上述读取修改修改写入写入变体,其中您有一个写入最后一个并取决于何时读取确定是否发生了两个修改 . 并非同时“像大多数现代硬件”一样没有意义,在单独的内核/模块中开始实际并行的事情最终会被序列化,如果它们针对的是内存中的同一个触发器/晶体管,最终必须等待对方先行 . 作为基于物理的我不会在未来几周/几个月/几年内看到这是不正确的 .

  • 2

    这是对的 . 与原始x86 CPU一样,x86_64 CPU无法读取或写入来自rsp的小于(在本例中为64位)字的任何内容 . 记忆 . 它通常不会读取或写入少于整个缓存行,但有一些方法可以绕过缓存,特别是在写入时(见下文) .

    但是,在这种情况下,Stroustrup指的是潜在的数据竞争(在可观察的水平上缺乏原子性) . 由于您提到的缓存一致性协议,此正确性问题与x86_64无关 . 换句话说,是的,CPU仅限于全字传输,但这是透明处理的,而您作为程序员通常不必担心它 . 实际上,从C 11开始的C语言保证了不同内存位置上的并发操作具有明确定义的行为,即您期望的行为 . 即使硬件不能保证这一点,实现也必须通过生成可能更复杂的代码来找到方法 .

    也就是说,出于两个原因,保持整个单词甚至缓存行总是涉及机器后级的事实仍然是一个好主意 .

    • 首先,这仅适用于编写设备驱动程序或设计设备的人员,内存映射I / O可能对其访问方式很敏感 . 例如,考虑一个在物理地址空间中公开64位只写命令寄存器的设备 . 然后可能需要:

    • 禁用缓存 . 读取缓存行,更改单个单词以及写回缓存行无效 . 此外,即使它有效,仍然存在命令可能丢失的巨大风险,因为CPU缓存不会很快写回 . 至少,页面需要配置为"write-through",这意味着写入立即生效 . 因此,x86_64页表条目包含控制CPU对此页面的缓存行为的标志 .

    • 确保整体总是在汇编级别写出单词 . 例如 . 考虑一种情况,你将值1写入寄存器,然后是2.编译器,特别是在优化空间时,可能决定只覆盖最低有效字节,因为其他字节已经被认为是零(即,普通的RAM),或者它可能会删除第一次写入,因为无论如何这个值似乎都会被立即覆盖 . 但是,这两者都不应该发生 . 在C / C中, volatile 关键字对于防止此类不合适的优化至关重要 .

    • 其次,这几乎适用于任何编写多线程程序的开发人员,缓存一致性协议,同时整齐地避免灾难,如果它是"abused"则可能会产生巨大的性能成本 .

    这是一个有点人为的 - 一个非常糟糕的数据结构的例子 . 假设您有16个线程正在解析文件中的某些文本 . 每个线程都有一个从0到15的 id .

    // shared state
    char c[16];
    FILE *file[16];
    
    void threadFunc(int id)
    {
        while ((c[id] = getc(file[id])) != EOF)
        {
            // ...
        }
    }
    

    这是安全的,因为每个线程在不同的内存位置上运行 . 但是,这些存储器位置通常驻留在同一高速缓存行上,或者最多分成两个高速缓存行 . 然后使用高速缓存一致性协议来正确地同步对 c[id] 的访问 . 这就解决了这个问题,因为这会强制每个其他线程等到高速缓存行变为专有可用之后再做 c[id] ,除非它已经在高速缓存行的核心上运行 . 假设有几个,例如如图16所示,核心,高速缓存一致性通常将高速缓存线从一个核心一直传送到另一个核心 . 出于显而易见的原因,这种效应称为"cache line ping-pong" . 它造成了一个可怕的性能瓶颈 . 这是错误共享的非常糟糕的情况的结果,即线程共享物理高速缓存行而没有实际访问相同的逻辑存储器位置 .

    与此形成对比的是,特别是如果需要额外的步骤来确保 file 数组驻留在自己的缓存行上,从性能角度来看,使用它将是完全无害的(在x86_64上),因为指针只能读取,大多数时候 . 在这种情况下,多个核心可以将高速缓存行作为只读 . 只有当任何核心尝试写入高速缓存行时,它必须告诉其他核心它将进入独占访问的高速缓存行 .

    (这大大简化了,因为有不同级别的CPU缓存,并且多个内核可能共享相同的L2或L3缓存,但它应该让您对问题有基本的了解 . )

  • 2

    Stroustrup并不是说没有机器可以执行小于其原始字大小的装载和存储,他说机器不能 .

    虽然这一开始似乎令人惊讶,但这并不是什么深奥的 .
    对于入门者,我们将忽略缓存层次结构,我们稍后会考虑到这一点 .
    假设CPU和内存之间没有缓存 .

    记忆的一个大问题是密度,试图将更多的比特放到最小的区域 .
    为了实现这一点,从电气设计的角度来看,尽可能宽地暴露总线是方便的(这有利于重用某些电信号,但我没有看到具体的细节) .
    因此,在需要大存储器的架构中(如x86)或简单的低成本设计是有利的(例如涉及RISC机器),存储器总线大于最小可寻址单元(通常是字节) .

    根据项目的预算和遗留情况,存储器可以单独暴露更宽的总线,或者与一些边带信号一起暴露以选择特定的单元 .
    这实际意味着什么?
    如果你看一下datasheet of a DDR3 DIMM,你会发现有64个DQ0-DQ63引脚来读/写数据 .
    这是数据总线,64位宽,一次8个字节 .
    这个8字节的东西在x86架构中非常有根据,以至于英特尔在其优化手册的WC部分中引用它,它说数据是从64字节填充缓冲区传输的(请记住:我们忽略了缓存现在,但这类似于高速缓存行的写入方式,以8字节的突发(希望,连续) .

    这是否意味着x86只能写QWORDS(64位)?
    不,相同的数据表显示每个DIMM都有DM0-DM7,DQ0-DQ7和DQS0-DQS7信号,用于屏蔽,指示和选通64位数据总线中的8个字节 .

    因此x86可以原生和原子地读写字节 .
    然而,现在很容易看出每个架构都不是这种情况 .
    例如,VGA视频内存是DWORD(32位)可寻址,并使其适合8086的字节可寻址世界,导致凌乱的位平面 .

    通常,特定目的体系结构(如DSP)在硬件级别上不能具有字节可寻址存储器 .

    有一个转折:我们刚刚谈到了内存数据总线,这是最低层可能 .
    某些CPU可以具有在字可寻址存储器之上构建字节可寻址存储器的指令 .
    那是什么意思?
    加载一个单词的一小部分很容易:只丢弃剩下的字节!
    不幸的是,我不记得架构的名称(如果它甚至存在!),其中处理器通过读取包含它的对齐字并在将其保存在寄存器中之前旋转结果来模拟未对齐字节的加载 .

    对于商店来说,问题更复杂:如果我们不能简单地写出我们刚刚更新的那个词的一部分,我们也需要编写未更改的剩余部分 .
    CPU或程序员必须读取旧内容,更新并将其写回 .
    这是一个读 - 修改 - 写操作,它是讨论原子性时的核心概念 .

    考虑:

    /* Assume unsigned char is 1 byte and a word is 4 bytes */
    unsigned char foo[4] = {};
    
    /* Thread 0                         Thread 1                 */
    foo[0] = 1;                        foo[1] = 2;
    

    有数据竞争吗?
    这在x86上是安全的,因为它们可以写入字节,但是如果架构不能呢?
    两个线程都必须读取整个 foo 数组,修改它并将其写回 .
    在伪C中,这将是

    /* Assume unsigned char is 1 byte and a word is 4 bytes */
    unsigned char foo[4] = {};
    
    /* Thread 0                        Thread 1                 */
    
    /* What a CPU would do (IS)        What a CPU would do (IS) */
    int tmp0 = *((int*)foo)            int tmp1 = *((int*)foo)
    
    /* Assume little endian            Assume little endian     */
    tmp0 = (tmp0 & ~0xff) | 1;         tmp1 = (tmp1 & ~0xff00) | 0x200;
    
    /* Store it back                   Store it back            */
    *((int*)foo) = tmp0;               *((int*)foo) = tmp1;
    

    我们现在可以看到Stroustrup正在谈论的内容:两个商店 *((int*)foo) = tmpX 相互阻碍,看看这个可能的执行顺序:

    int tmp0 = *((int*)foo)                   /* T0  */ 
    tmp0 = (tmp0 & ~0xff) | 1;                /* T1  */        
    int tmp1 = *((int*)foo)                   /* T1  */
    tmp1 = (tmp1 & ~0xff00) | 0x200;          /* T1  */
    *((int*)foo) = tmp1;                      /* T0  */
    *((int*)foo) = tmp0;                      /* T0, Whooopsy  */
    

    如果C没有内存模型,那么这些类型的讨厌将是特定于实现的细节,使C在多线程环境中成为无用的编程语言 .

    考虑到玩具示例中描述的情况有多常见,Stroustrup强调了明确定义的内存模型的重要性 .
    将内存模型正式化是一项艰苦的工作,这是一个令人筋疲力尽,容易出错和抽象的过程,所以我也看到了Stroustrup的一些自豪感 .

    我没有刷新C内存模型,但更新了不同的数组元素is fine .
    这是一个非常有力的保证 .

    我们遗漏了缓存,但这并没有真正改变任何东西,至少对于x86案例 .
    x86通过缓存写入内存,缓存以64字节为单位被逐出 .
    在内部,每个核心可以原子地更新任何位置的线,除非加载/存储穿过线边界(例如,通过在其末端附近写入) .
    这可以通过自然对齐数据来避免(你可以证明吗?) .

    在多代码/套接字环境中,高速缓存一致性协议确保一次只允许CPU自由写入高速缓存的内存行(使其处于独占或修改状态的CPU) .
    基本上,MESI协议族使用类似锁定的概念找到了DBMS .
    出于写入目的,这对于不同的CPU具有不同的存储区域的效果 .
    所以它并没有真正影响上面的讨论 .

  • 11

    我认为这不是一个非常准确,清晰或有用的陈述 . 更准确地说,现代CPU无法加载或存储小于缓存行的任何内容 . (虽然对于不可缓存的内存区域不是这样,例如对于MMIO . )

    做一个假设的例子可能会更好,而不是暗示真正的硬件是这样的 . 但是,如果我们尝试,我们可能会找到一种不那么明显或完全错误的解释,这可能是Stroustrup在撰写此内容以介绍内存模型主题时的想法 . (对不起,这个答案太长了;我最后写了很多东西,同时猜测他的意思和相关主题......)

    或者这可能是高级语言设计者不是硬件专家,或者至少偶尔会做出错误陈述的另一种情况 .


    我认为Stroustrup正在讨论CPU如何在内部工作以实现字节存储指令 . 他建议没有定义明确且合理的内存模型的CPU可能会在高速缓存行中使用包含单词的非原子RMW实现字节存储,或者在没有高速缓存的CPU的内存中实现 .

    Even this weaker claim about internal (not externally visible) behaviour is not true for any high-performance CPUs I'm aware of, including modern x86 . 现代Intel CPU对字节存储没有吞吐量损失,甚至没有跨越缓存线边界的未对齐字或向量存储 . AMD类似 . 如果其中任何一个必须执行RMW循环,因为存储提交到L1D缓存,它将干扰负载带宽 .


    Alpha AXP ,1992年的高性能RISC设计,着名(并且在现代非DSP ISA中独一无二) omitted byte load/store instructions until Alpha 21164A (EV56) in 1996 . 显然,他们并不认为word-RMW是实现字节存储的可行选择,因为仅实现32位和64位对齐存储的一个优点是L1D缓存的ECC效率更高 . "Traditional SECDED ECC would require 7 extra bits over 32-bit granules (22% overhead) versus 4 extra bits over 8-bit granules (50% overhead)."(@Paul A. Clayton关于字与字节寻址的答案还有一些其他有趣的计算机架构 . )如果使用word-RMW实现字节存储,你仍然可以使用字粒度进行错误检测/纠正 .

    由于这个原因,当前的Intel CPU仅在L1D中使用奇偶校验(不是ECC) . 请参阅this Q&A关于硬件(不)消除"silent stores":在写入之前检查缓存的旧内容,以避免标记线脏,如果匹配则需要RMW而不仅仅是存储,这是一个主要障碍 .

    我假设其他(非x86)现代CPU设计并未将RMW视为将字节存储提交到L1D缓存的选项 . 但另请参阅 Are there any modern/ancient CPUs / microcontrollers where a cached byte store is actually slower than a word store? 它不太可能他们对RAM进行外部RMW . 希望有人会回答这个问题 .

    Word-RMW对于MMIO字节存储也不是一个有用的选项,所以除非你的架构不需要对IO进行某种特殊处理(比如Alpha's sparse I/O space,其中字加载/存储被映射到字节加载/存储所以它可以使用商用PCI卡而不需要没有字节IO寄存器的特殊硬件 .

    作为@Margaret points out,DDR3内存控制器可以通过设置屏蔽突发其他字节的控制信号来进行字节存储 . 将此信息提供给内存控制器(对于未缓存的存储)的相同机制也可以将该信息与加载或存储一起传递到MMIO空间 . 因此,即使在面向突发的存储器系统上,也存在用于真正进行字节存储的硬件机制,并且它可能更简单并且对于MMIO正确性更好 .

    How many and what size cycles will be needed to perform longword transferred to the CPU shows how a ColdFire microcontroller signals the transfer size (byte/word/longword/16-byte line) with external signal lines, 即使32位宽的内存连接到32位数据总线,它也可以进行字节加载/存储 . 像这样的东西大概是大多数内存总线设置的典型(但我不知道) . ColdFire示例很复杂,还可以配置为使用16位或8位内存,为更广泛的传输采用额外的周期 . 但是没关系,重要的是它有传输大小的外部信号,告诉内存HW它实际写入哪个字节 .


    Stroustrup's next paragraph is

    “C内存模型保证两个执行线程可以更新和访问不同的内存位置而不会相互干扰 . 这正是我们天真期望的 . 编译器的工作是保护我们免受有时非常奇怪和微妙的行为的影响现代硬件 . 编译器和硬件组合如何实现,这取决于编译器......“

    显然他认为真正的现代硬件可能无法提供“安全”的字节加载/存储 . 设计硬件内存模型的人与C / C人员一致,并意识到如果字节存储指令可以踩到相邻字节,那么它们对程序员/编译器就没有用 .

    All modern (non-DSP) architectures except early Alpha AXP have byte store and load instructions, and AFAIK these are all architecturally defined to not affect neighbouring bytes. 然而,他们在硬件方面做到了这一点,软件并不是一个非常注重字的ISA .

    但是,他实际上并没有声称大多数现代硬件需要任何特殊的编译器支持来实现C内存模型的这一部分,只是有些人可能会这样做 . 也许他真的只是讨论第二段中的字可寻址DSP(其中C和C实现经常使用16或32位 char ,正如Stroustrup所讨论的那种编译器工作方式 . )


    Most "modern" CPUs (including all x86) have an L1D cache . 它们将获取整个缓存行(通常为64个字节)并在每个缓存行的基础上跟踪脏/非脏 . So two adjacent bytes are pretty much exactly the same as two adjacent words, if they're both in the same cache line. 写入一个字节或一个字将导致整行的读取,并最终写回整行 . 见Ulrich Drepper的What Every Programmer Should Know About Memory . 你是正确的MESI(或像MESIF / MOESI这样的派生物)确保这不是问题 . (但同样,这是因为硬件实现了一个理智的内存模型 . )

    存储只能在线路处于修改状态(MESI)时提交到L1D缓存 . 因此,即使内部硬件实现对于字节来说很慢并且需要额外的时间将字节合并到高速缓存行中的包含字中,它实际上是原子读取修改写入,只要它不允许该行无效并且重新开始 . - 读取和写入之间的获取 . (While this cache has the line in Modified state, no other cache can have a valid copy) . 请参阅@old_timer's comment进行相同的操作(但也适用于内存控制器中的RMW) .

    这更容易比例如来自寄存器的原子 xchgadd 也需要ALU和寄存器访问,因为所涉及的所有HW都处于相同的流水线阶段,这可以简单地停止一两个额外的循环 . 那个's obviously bad for performance and takes extra hardware to allow that pipeline stage to signal that it'停滞不前 . 这不是第一个主张,因为他在谈论一个没有记忆模型的假设ISA,但它仍然是一个延伸 .

    在单核微控制器上,用于缓存字节存储的内部字RMW将更加合理,因为在原子RMW缓存字更新期间不会有来自其他内核的无效请求,它们必须延迟响应 . 但这对于无法缓存的区域的I / O没有帮助 . 我说微控制器,因为其他单核CPU设计通常支持某种多插槽SMP .


    许多RISC ISA都是一个单独的问题(当负载跨越两个缓存行甚至页面时,难以处理这种情况,这不会发生字节或对齐的半字) . 然而,越来越多的ISA正在为最近版本中的未对齐加载/存储添加有保障的支持 . (例如2014年MIPS32/64 Release 6,我认为AArch64和最近的32位ARM) .


    这本书的出版于2013年出版,当时Alpha已经死了多年 . 第一版是published in 1985,当时RISC是一个新的大创意(例如1983年的斯坦福MIPS,according to Wikipedia's timeline of computing HW,但"modern"当时的CPU是字节存储的字节可寻址.Cyber CDC 6600是可以字寻址的,可能还在,但不能不称为现代 .

    即使是非常面向字的RISC机器,如MIPSSPARC,也有字节存储和字节加载(带符号或零扩展)指令 . 它们不支持未对齐的字加载,简化缓存(或者如果没有缓存则可以访问内存)和加载端口,但是您可以使用一条指令加载任何单个字节,更重要的是存储一个字节而不重写周围的字节 .

    我认为如果针对没有字节存储的Alpha ISA版本,Alpha上的C 11(在语言中引入了线程感知内存模型)将需要使用32位 char . 或者它必须使用带有LL / SC的软件atomic-RMW,因为它无法证明没有其他线程可以有一个指针让它们写入相邻的字节 .


    IDK how slow byte load/store instructions are in any CPUs where they're implemented in hardware but not as cheap as word loads/stores . 只要使用 movzx/movsx 来避免部分寄存器错误依赖或合并停顿,x86上的字节加载便宜 . On AMD pre-Ryzen, movsx/movzx needs an extra ALU uop, but otherwise zero/sign extension is handled right in the load port on Intel and AMD CPUs.)主要的x86缺点是你需要一个单独的加载指令而不是使用内存操作数作为ALU指令的源(如果你只是在一个字节寄存器中添加一个字节,那么x86基本没有下行.RISC加载-store ISA总是需要单独的加载和存储指令.x86字节存储并不比32位存储更昂贵 .

    作为性能问题,具有慢字节存储的硬件的良好C实现可能会将每个 char 放在它自己的字中并尽可能使用字加载/存储(例如,对于结构体外的全局变量,以及堆栈上的本地变量) . IDK,如果有任何MIPS / ARM的实际实现/无论是慢速字节加载/存储,但如果是这样,gcc可能有 -mtune= 选项来控制它 .

    That doesn't help for char[],或者在您不知道它指向何处时取消引用 char * . (这包括你用于MMIO的 volatile char* . )因此,让编译器链接器将 char 变量放在单独的单词中并不是一个完整的解决方案,只是在真正的字节存储很慢时才会出现性能问题 .


    PS: More about Alpha:

    由于很多原因,Alpha很有趣:为数不多的64位ISA之一,而不是现有32位ISA的扩展 . 作为最新的清洁版ISA之一,Itanium是几年后的另一个尝试了一些简洁的CPU架构理念 .

    来自Linux Alpha HOWTO . 当引入Alpha架构时,它在RISC架构中是独一无二的,可以避开8位和16位负载和存储 . 它支持32位和64位加载和存储(长字和四字,在Digital的命名法中) . 共同架构师(Dick Sites,Rich Witek)通过引用优势证明了这一决定:缓存和内存子系统中的字节支持往往会减慢32位和64位数量的访问速度 . 字节支持使得很难在高速缓存/存储器子系统中构建高速纠错电路 . Alpha通过提供强大的指令来补偿64位寄存器中的字节和字节组 . 字符串的标准基准操作(例如,一些字节基准测试)表明Alpha在字节操作方面表现很好 .

  • 7

    x86 CPU不仅能够读写单个字节,而且所有现代通用CPU都能够实现这一点 . 更重要的是,大多数现代CPU(包括x86,ARM,MIPS,PowerPC和SPARC)都能够以原子方式读取和写入单个字节 .

    我不确定Stroustrup指的是什么 . 曾经有一些无法进行8位字节寻址的字可寻址机器,如Cray,而且Peter Cordes提到早期的Alpha CPU不支持字节加载和存储,但今天唯一的CPU无法支持字节加载和存储是在特定应用中使用的某些DSP . 即使我们假设他意味着大多数现代CPU没有原子字节加载和存储,但对于大多数CPU而言并非如此 .

    但是,使用它们需要简单的原子载荷和存储 . 例如,编译器仍然可以生成Stroustrup描述的代码,使用单个字加载指令加载 bc 作为优化 .

    因此,虽然您确实需要一个定义良好的内存模型,但只有这样才能强制编译器生成您期望的代码,问题不在于现代CPU无法加载或存储任何小于单词的内容 .

相关问题