当前位置: 首页 > 知识库问答 >
问题:

处理器如何读取内存?

陈毅
2023-03-14

我正在尝试重新实现malloc,我需要理解对齐的目的。据我所知,如果内存对齐,代码将执行得更快,因为处理器不必采取额外步骤来恢复被剪切的内存位。我想我理解64位处理器读取64位逐64位内存。现在,让我们想象一下,我有一个有序的结构(没有填充):一个char、一个short、一个char和一个int。为什么short会错位?我们有区块中的所有数据!为什么地址必须是2的倍数。整数和其他类型的问题是一样的吗?

我还有第二个问题:使用我之前提到的结构,处理器如何在读取其64位时知道前8位对应于char,然后接下来的16位对应于短等...?

共有3个答案

时衡虑
2023-03-14

据我所知,如果内存对齐,代码将执行得更快,因为处理器不必采取额外步骤来恢复被剪切的内存位。

这不一定是一个执行问题,x86有可变长度的指令,从单个8位指令开始,最多有几个字节到几个字节,这都是关于未对齐的。但他们已采取措施,在很大程度上缓解了这种局面。

如果我在处理器的边缘有一个64位总线,这并不意味着芯片的边缘,而是意味着核心的边缘。另一方面是一个知道总线协议的内存控制器,它是地址开始被解码的第一个地方,事务开始向目的地分解其他总线。

它是非常特定于体系结构和总线设计的,随着时间的推移,您可以拥有具有不同总线的体系结构或不同版本,例如,您可以获得具有64总线或32位总线的arm。但假设我们有一个不典型的情况,即总线是64位宽的,并且该总线上的所有事务都在64位边界上对齐。

如果我要对0x1000进行64位写入,那将是一个单总线事务,现在是某种写入地址总线,具有一些id x和长度为0(n-1),然后另一侧ack我看到你想用id x进行写入,我准备好获取你的数据了。然后处理器使用带有id x的数据总线发送数据,每64位一个时钟,这是一个单独的64位,所以总线上有一个时钟。可能会有一个ack回来,也可能不会。

但如果我想对0x1004进行64位写入,那么会发生两个事务,一个完整的64位地址/数据事务位于地址0x1000,只有四个字节的通道启用了通道4-7(表示地址0x1004-0x1007处的字节)。然后在0x1008处完成事务,启用4字节通道,通道0-3。因此,总线上的实际数据移动从一个时钟移动到两个时钟,但到达这些数据周期的握手开销也是两倍。在那辆巴士上,很明显,整个系统设计是怎样的,尽管你可能感觉不到,也可能不得不做很多事情才能感觉到。但效率低下是存在的,是否被噪音所掩盖。

我想我明白64位处理器读取64位乘64位内存。

32位ARM现在有64位总线,例如ARMv6和ARMv7随附或可以。

现在,让我们想象一下,我有一个有序的结构(没有填充):一个char、一个short、一个char和一个int。为什么short会错位?我们有区块中的所有数据!为什么地址必须是2的倍数。整数和其他类型的问题是一样的吗?

unsigned char a   0x1000
unsigned short b  0x1001
unsigned char c   0x1003
unsigned int d    0x1004

您通常会使用代码中的结构项某物. a某物. b某物. c某物. d。当您访问某物. b时,这是对总线的16位事务。在64位系统中,您是正确的,如果按照我的地址对齐,那么当您执行x=某物. b时,整个结构正在被读取,但处理器将丢弃除字节通道1和2之外的所有内容(丢弃0和3-7),然后如果您访问某物. c,它将在0x1000执行另一个总线事务并丢弃除通道3之外的所有内容。

当你写东西的时候。b对于64位总线,仅启用字节通道1和2。现在更麻烦的是,如果有一个缓存,它很可能也是由64位ram构成的,以配合此总线,不必这样做,但让我们假设它这样做了。您想通过缓存写入某些内容。b、 0x1000处的写入事务,字节通道1和2启用0,3-7禁用。缓存最终得到这个事务,它必须在内部执行读-修改-写操作,因为它不是一个完整的64位事务(启用了所有通道),所以从性能角度来看,读-修改-写操作也会受到影响(对于上面未对齐的64位写操作也是如此)。

短是不对齐的,因为当打包其地址lsbit设置时,要对齐8位中的16位项目是字节世界需要为零,对于要对齐的32位项目,其地址的较低两位为零、64位、三个零等。

根据系统的不同,您最终可能会使用32位或16位总线(现在内存不够),因此您最终可以进行多次传输。

您的高效处理器(如MIPS和ARM)采用了对齐指令的方法,甚至在某物中也强制对齐事务。b在32或64位总线上没有惩罚的情况。这种方法的性能超过了内存消耗,因此指令在某种程度上是浪费的,以便更有效地获取和执行指令。数据总线同样要简单得多。当构造像C中的结构这样的高级概念时,为了获得性能,在对齐结构中的每个项时,填充会造成内存浪费。

unsigned char a   0x1000
unsigned short b  0x1002
unsigned char c   0x1004
unsigned int d    0x1008

例如

我还有第二个问题:使用我之前提到的结构,处理器如何在读取其64位时知道前8位对应于char,然后接下来的16位对应于短等...?

unsigned char c   0x1003

编译器在地址0x1003处生成一个单字节大小的读取,这将转换为具有该地址的特定指令,处理器生成总线事务来执行该操作,然后处理器总线的另一端执行其工作,依此类推。

编译器通常不会将该结构的压缩版本转换为提供所有项的单个64位事务,而是为每个项刻录一个64位总线事务。

可能的情况是,根据指令集、预取器、缓存等,您不需要在高级别使用结构,而是创建一个64位整数,然后在代码中完成工作,那么您可能会获得性能,也可能不会获得性能。在大多数运行缓存等的体系结构上,这并不会表现得更好,但是,当您进入ram上可能有一定数量的等待状态、闪存上可能有一定数量的等待状态或任何代码存储的嵌入式系统时,您会发现,您需要的不是更少的指令和更多的数据事务,而是更多的指令和更少的数据事务。代码是线性的,如读、掩码和移位、掩码和移位等代码部分。指令存储可能具有用于线性事务的突发模式,但数据事务占用的时钟数量与它们占用的时钟数量相同。

一个折衷的方法是将所有内容都设置为32位变量或64位变量,然后将所有内容对齐,并以使用更多内存为代价实现相对良好的性能。

因为人们不理解对齐,被x86编程宠坏了,选择跨编译域使用结构(这是一个糟糕的主意),ARMs和其他平台都容忍未对齐的访问,所以您可以在这些平台上感受到性能的影响,因为如果一切都对齐,它们的效率会非常高,但是,当您执行一些未对齐的操作时,它只会生成更多的总线事务,使所有操作都需要更长的时间。因此,较旧的ARM默认会出现故障,arm7可以禁用故障,但会围绕字旋转数据(交换字中16位值的好技巧),而不是溢出到下一个字中,后来的架构默认为对齐时不出错,或者大多数人将其设置为对齐时不出错,并按照人们的希望/预期读取/写入未对齐的传输。

你的电脑中每有一个x86芯片,你就会在同一台电脑或挂在电脑上的外围设备(鼠标、键盘、显示器等)中拥有几个(如果不是少数的话)非x86处理器。其中很多是8位8051和z80,但也有很多是基于arm的。因此,不仅所有手机和平板电脑的主处理器都在进行许多非x86开发。其他人希望低成本和低功耗,从而提高编码效率,既能提高总线性能,使时钟变慢,又能平衡整体代码/数据使用,以降低闪存/内存的成本。

在x86平台上强制执行这些对齐问题是非常困难的—要克服其体系结构问题需要大量开销。但你可以在更高效的平台上看到这一点。这就像一辆火车和一辆跑车,有东西从火车上掉下来,一个人从火车上跳下来或跳上,有如此多的动量,以至于一点都没有被注意到,但是改变一下跑车上的质量,你就会感觉到。因此,在x86上尝试这样做,如果您甚至能够想出如何做到这一点,就必须更加努力。但在其他平台上更容易看到效果。除非你找到一个8086芯片,我怀疑你能感觉到其中的差异,否则你必须拿出我的手册来确认。

如果你足够幸运,可以访问芯片源/模拟,那么你可以看到这种事情到处发生,并且可以真正开始手动调整你的程序(针对该平台)。同样,您可以看到缓存、写缓冲、各种形式的指令预取等对整体性能的影响,有时会创建并行的时间段,其他效率不高的事务可以隐藏,或者创建有意的空闲周期,以便需要额外时间的事务可以有一个时间片。

施誉
2023-03-14

现代处理器和内存是为了尽可能优化内存访问而构建的。当前访问内存的一种方式是不是逐字节寻址,而是通过更大块的地址,例如通过8字节块。这样就不需要地址的3个低位。为了访问块中的某个字节,进程需要在对齐的地址获取块,然后移位并屏蔽字节。所以,速度变慢了。

当结构中的字段未对齐时,存在降低访问速度的风险。因此,最好对齐它们。

但对齐要求是基于底层平台的。对于支持字访问(32位)的系统,4字节对齐是可以的,否则可以使用8字节或其他方式。编译器(和libc)知道这些需求。

因此,在您的示例char,short,char中,如果没有填充,short将以奇数字节位置开始。要访问它,系统可能需要读取结构的64位字,然后将其右移1个字节,然后屏蔽2个字节,以便为您提供此字节。

杜志
2023-03-14

影响甚至可以包括正确性,而不仅仅是性能:C未定义行为(UB)导致可能的分段错误或其他不当行为,例如,如果您有一个对象不满足对齐(短)。(在加载/存储指令默认需要对齐的ISA上预计会出现故障,例如SPARC和MIPS64r6之前的MIPS。甚至在编译器优化循环后的x86上也可能出现故障,尽管x86 ami允许未对齐的加载/存储,但某些16字节或更宽的SIMD除外。)

或者如果原子int没有对齐(\u原子int),则撕裂原子操作。

(在任何给定的ABI中,通常小于或等于某个大小的寄存器宽度或更宽的寄存器)。

malloc应该使用alignof(max\u align\u t)返回内存,因为您没有关于如何使用分配的任何类型信息。

对于小于sizeof(max\u align\u t)的分配,如果需要,可以返回仅自然对齐的内存(例如,4字节对齐的4字节分配),因为您知道存储不能用于任何具有更高对齐要求的内存。

像动态分配的alignas(16)int32\u t foo之类的过度对齐的东西需要使用特殊的分配器,如C11 aligned\u alloc。如果您正在实现自己的分配器库,您可能希望支持aligned\u realloc和aligned\u calloc,以填补ISO C无缘无故留下的空白。

并且,如果分配大小不是对齐的倍数,请确保您没有执行头脑呆滞的ISO C 17要求,以允许分配失败。没有人想要一个分配器拒绝从16字节边界开始的101个浮点的分配,或者为了更好的透明hugepages而拒绝更大的浮点分配。aligned\u alloc功能要求以及如何解决AVX加载/存储操作的32字节对齐问题?

我想我明白64位处理器读取64位乘64位内存

不。数据总线宽度和突发大小,以及加载/存储执行单元最大宽度或实际使用宽度,不必与整数寄存器的宽度相同,或者CPU定义其位。(在现代高性能CPU中通常不是。例如32位P5 Pentium有一个64位总线;现代32位ARM有执行原子64位访问的加载/存储对指令。)

处理器将整个缓存线从DRAM/L3/L2缓存读取到L1d缓存中;现代x86上的64字节;其他一些系统上为32字节。

当读取单个对象或数组元素时,它们以元素宽度从L1d缓存中读取。e、 g.对于2字节加载/存储,阵列可能只会从与2字节边界对齐中受益。

或者,如果编译器使用SIMD对循环进行向量化,则可以一次读取16或32个字节的uint16\u t数组,即8或16个元素的SIMD向量。(甚至64与AVX512)。将数组与预期的向量宽度对齐可能会有所帮助;未对齐的SIMD加载/存储在不跨越缓存线边界的现代x86上快速运行。

缓存线拆分,尤其是页面拆分是现代x86因未对齐而变慢的地方;缓存线内未对齐通常不是因为它们将晶体管用于快速未对齐的加载/存储。其他一些ISA在任何未对齐时都会减慢速度,甚至在缓存线内也会出现故障。解决方案相同:给定类型自然对齐:alignof(T)=sizeof(T)。

在您的结构示例中,即使未对齐,现代x86 CPU也不会受到任何惩罚。在任何正常ABI中,对准(int)=4,因此整个结构具有对准(struct)=4,因此char;短;char块从4字节边界开始。因此,包含在单个4字节dword中,不会跨越任何更宽的边界。AMD和Intel都以最高效率处理此问题。(x86 ISA保证对它的访问是原子的,甚至是未缓存的,在与P5 Pentium或更高版本兼容的CPU上:为什么在x86上对自然对齐的变量原子进行整数赋值?)

一些非x86 CPU会因未对齐的短路而受到惩罚,或者必须使用其他指令。(因为您知道相对于对齐的32位块的对齐方式,所以对于加载,您可能会执行32位加载和移位。)

因此,访问包含short的单个字是没有问题的,但问题是加载端口硬件要将short提取并零扩展(或符号扩展)到完整寄存器中。这就是x86利用晶体管来提高速度的原因。(@Eric对这个问题的前一个版本的回答更详细地介绍了所需的换档。)

将未对齐的存储提交回缓存也不是小事。例如,L1d缓存可能在32位或64位块(我称之为“缓存字”)中具有ECC(针对位翻转的纠错)。因此,仅写入缓存字的一部分是一个问题,以及将其转移到您要访问的缓存字内的任意字节边界。(在存储缓冲区中合并相邻的窄存储可以产生全幅提交,从而避免RMW周期来更新单词的一部分,在以这种方式处理窄存储的缓存中)。请注意,我现在说“word”是因为我谈论的硬件更面向单词,而不是像现代x86那样围绕未对齐的加载/存储进行设计。请参阅是否有任何现代CPU缓存字节存储实际上比字存储慢?(存储单个字节仅比未对齐的稍微简单一点)

(如果跨越两个缓存字,它当然需要单独的RMW周期,每个字节一个。)

当然,是不对齐的,原因很简单,对准(短)=2,它违反了这个ABI规则(假设ABI确实有这个规则)。因此,如果您将指向它的指针传递给其他函数,您可能会遇到麻烦。尤其是在具有错误对错误对齐负载的CPU上,而不是在运行时发现错误对齐时由硬件处理这种情况。然后您会遇到这样的情况:为什么对mmap'ed内存的未对齐访问有时会在AMD64上出现分段错误?其中GCC自动矢量化预计通过执行多个2字节元素标量来达到16字节边界,因此违反ABI会导致x86上的分段错误(通常可以容忍未对齐。)

有关内存访问的完整详细信息,从DRAM RAS/CAS延迟到缓存带宽和对齐,请参阅每个程序员都应该了解的内存?它几乎仍然相关/适用

内存对齐的目的也有一个很好的答案。SO的内存对齐标签中还有很多其他好的答案。

有关现代Intel加载/存储执行单元的更详细信息,请参阅:https://electronics.stackexchange.com/questions/329789/how-can-cache-be-that-fast/329955#329955

处理器在读取其64位时,如何知道前8位对应一个字符,然后下16位对应一个短字符等等。。。?

它并没有这样做,只是它运行的指令以这种方式处理数据。

在ash/机器代码中,一切都只是字节。每条指令都确切地指定了如何处理哪些数据。由编译器(或人类程序员)在原始字节数组(主存储器)之上实现具有类型的变量和C程序的逻辑。

我的意思是,在ash中,您可以运行任何您想要的加载或存储指令,并且由您在正确的地址上使用正确的指令。您可以将重叠两个相邻int变量的4个字节加载到浮点寄存器中,然后在其上运行addss(单精度FP add),CPU不会抱怨。但您可能不想这样做,因为让CPU将这4个字节解释为IEEE754 binary32浮点数不太可能有意义。

 类似资料:
  • 我有一个批处理步骤 读取器和处理器流程如何工作?读取器是读取块并等待处理器处理它,还是一次读取所有块。

  • 我们的SpringBatch作业只有一个步骤,包括ItemReader、ItemProcessor和ItemWriter。我们用不同的参数同时运行同一个作业。ItemReader是有状态的,因为它包含从中读取的输入流。 因此,我们不希望ItemReader的相同实例用于每个JobInstance(作业参数)调用。 我不太确定哪种是这种情况的最佳“范围界定”。 1) 该步骤是否应该以@JobScop

  • 根据已接受的答案代码,对该代码的以下调整对我起作用: 我已经将这个问题更新到了一个可以正确循环的版本,但是由于应用程序将扩展,能够处理并行是很重要的,我仍然不知道如何在运行时用javaconfig动态地做到这一点... 基于查询列表(HQL查询),我希望每个查询都有一个读取器-处理器-写入器。我当前的配置如下所示: 工单 处理机 作家 目前,该过程对于单个查询来说工作得很好。然而,我实际上有一个查

  • 我刚开始使用Spring批处理,我有一个特殊问题。我希望使用从3个不同的jpa查询中获取结果,并分别处理它们,然后使用将它们写入一个统一的XML文件。 对于eg,生成的XML看起来像是,

  • 我必须使用Spring Batch配置一个作业。是否可以有一个单线程的项目阅读器,但多线程处理器? 在这种情况下,ItemReader将通过从数据库中读取工作项(通过执行预定义的查询)来创建要处理的工作项,每个处理器将并行处理项/块。

  • 项目读取器将数据从特定源代码读入Spring批处理应用程序,而项目写入器将数据从Spring Batch应用程序写入特定目标。 Item处理器是一个包含处理代码的类,该代码处理读入spring批处理的数据。 如果应用程序读取条记录,则处理器中的代码将在每条记录上执行。 块(chunk)是该tasklet的子元素。 它用于执行读取,写入和处理操作。 可以在如下所示的步骤中配置使用此元素的读取器,写入