我知道关于这个主题的多个问题,但是,我没有看到任何明确的答案或任何基准测量.因此,我创建了一个简单的程序,它使用两个整数数组.第一个数组a非常大(64 MB),第二个数组b很小,适合L1缓存.程序迭代a并将其元素添加到b模块化意义上的相应元素中(当到达结束时b,程序从其开始再次开始).测量的不同大小的L1缓存未命中数b如下:
测量是在具有32 kiB L1数据高速缓存的Xeon E5 2680v3 Haswell型CPU上进行的.因此,在所有情况下,都b适合L1缓存.然而,大约16 kiB的b内存占用量大大增加了未命中数.这可能因为两者的负载预期a并b导致缓存线失效从一开始b在这一点上.
绝对没有理由保留a缓存中的元素,它们只使用一次.因此,我运行一个具有非时间负载a数据的程序变体,但未命中数没有改变.我还运行了一个非暂时预取a数据的变体,但仍然有相同的结果.
我的基准代码如下(没有显示非时间预取的变体):
int main(int argc, char* argv[])
{
uint64_t* a;
const uint64_t a_bytes = 64 * 1024 * 1024;
const uint64_t a_count = a_bytes / sizeof(uint64_t);
posix_memalign((void**)(&a), 64, a_bytes);
uint64_t* b;
const uint64_t b_bytes = atol(argv[1]) * 1024;
const uint64_t b_count = b_bytes …Run Code Online (Sandbox Code Playgroud) 我对编写一个memcpy()教育练习感兴趣.我不会写一篇关于我做了什么和没想过的论文,但这里
有一些人的实现:
__forceinline // Since Size is usually known,
// most useless code will be optimized out
// if the function is inlined.
void* myMemcpy(char* Dst, const char* Src, size_t Size)
{
void* start = Dst;
for ( ; Size >= sizeof(__m256i); Size -= sizeof(__m256i) )
{
__m256i ymm = _mm256_loadu_si256(((const __m256i* &)Src)++);
_mm256_storeu_si256(((__m256i* &)Dst)++, ymm);
}
#define CPY_1B *((uint8_t * &)Dst)++ = *((const uint8_t * &)Src)++
#define CPY_2B *((uint16_t* &)Dst)++ = *((const uint16_t* &)Src)++
#define CPY_4B …Run Code Online (Sandbox Code Playgroud) OpenGL 和 Vulkan 都允许分别使用glMapBuffer和获取指向部分 GPU 内存的指针vkMapMemory。他们都给void*映射的内存一个。要将其内容解释为某些数据,必须将其强制转换为适当的类型。最简单的示例可能是转换为 afloat*以将内存解释为浮点数或向量或类似数组。
似乎任何类型的内存映射在 C++ 中都是未定义的行为,因为它没有内存映射的概念。但是,这并不是真正的问题,因为该主题超出了 C++ 标准的范围。但是,仍然存在一个问题volatile。
在链接的问题中,指针被额外标记为volatile因为它指向的内存内容可以以编译器在编译期间无法预料的方式进行修改。这似乎是合理的,尽管我很少看到人们volatile在这种情况下使用(更广泛地说,这个关键字现在似乎很少使用)。
同时在这个问题中,答案似乎是使用volatile是不必要的。这是因为他们所说的内存是映射使用的mmap,然后msync可以被视为修改内存,这类似于在 Vulkan 或 OpenGL 中显式刷新它。恐怕这不适用于 OpenGL 和 Vulkan。
如果内存被映射为未映射GL_MAP_FLUSH_EXPLICIT_BIT或根本VK_MEMORY_PROPERTY_HOST_COHERENT_BIT不需要刷新,则内存内容会自动更新。即使通过使用手动刷新内存,vkFlushMappedMemoryRanges或者glFlushMappedBufferRange这些函数实际上都没有将映射指针作为参数,因此编译器也不可能知道它们修改了映射内存的内容。
因此,是否有必要将指向映射 GPU 内存的指针标记为volatile?我知道从技术上讲这都是未定义的行为,但我问的是在实际硬件中实际需要什么。
顺便说一下,无论是Vulkan 规范还是OpenGL 规范都没有提到volatile限定符。
编辑:将内存标记为volatile会导致性能开销吗?
受到最近关于SO的问题和给出的答案的启发,这让我感到非常无知,我决定花一些时间来学习更多有关CPU缓存的知识,并编写了一个小程序来验证我是否正确地完成了这一切(大多数情况下)可能不是,我害怕).我将首先写下构成我期望的假设,所以如果错误的话,你可能会阻止我.基于我所读到的,一般来说:
n三通关联高速缓存被分成s组,每组包含n行,每行具有固定大小L;A可以被映射到任何所述的n的高速缓存行一个集;A映射地址的集合可以通过将地址空间拆分为每个大小为一个高速缓存行A的插槽,然后计算插槽(I = A / L)的索引,最后执行模运算以将索引映射到目标中来找到. set T(T = I % s);我的第一个问题是:这些假设是否正确?
假设它们是,我尝试使用这些概念,所以我实际上可以看到它们对程序产生了具体的影响.我写了一个简单的测试,它分配一个B字节的内存缓冲区,并从缓冲区的开头以固定的给定步长 增量重复访问该缓冲区的位置(意味着如果是14,步骤是3,我只重复访问位置0 ,3,6,9和12 - 如果是13,14或15 ,则同样如此:BB
int index = 0;
for (int i = 0; i < REPS; i++) …Run Code Online (Sandbox Code Playgroud) x86/x86_64体系结构的每个现代高性能CPU都有一些数据缓存层次结构:L1,L2,有时是L3(在极少数情况下是L4),从/向主RAM加载的数据缓存在其中一些中.
有时程序员可能希望某些数据不会缓存在某些或所有缓存级别中(例如,当想要memset 16 GB的RAM并将某些数据保留在缓存中时):有一些非时间(NT)指令用于这就像MOVNTDQA(/sf/answers/2596471/ http://lwn.net/Articles/255364/)
但有没有一种编程方式(对于某些AMD或Intel CPU系列,如P3,P4,Core,Core i*,......)完全(但暂时)关闭部分或全部级别的缓存,以改变每个内存的方式访问指令(全局或某些应用程序/ RAM区域)使用内存层次结构?例如:关闭L1,关闭L1和L2?或更改每次存储器访问类型CR0 ??? SDM vol3a页的"未缓存的" UC(CD + NW位423 424,425和" 仅适用于基于处理器的三级缓存禁止标志,位在IA32_MISC_ENABLE MSR 6(可用英特尔NetBurst微体系结构) - 允许禁用和启用L3缓存,独立于L1和L2缓存.").
我认为这样的行动将有助于保护数据免受缓存侧通道攻击/泄漏,如窃取AES密钥,隐蔽缓存通道,Meltdown/Spectre.虽然这种禁用会产生巨大的性能成本.
PS:我记得多年前在一些技术新闻网站上发布的这样一个程序,但现在找不到它.将一些神奇的值写入MSR只是一个Windows exe,并使每个Windows程序运行得很慢.缓存关闭直到重新启动或直到使用"撤消"选项启动程序.
有没有办法在不触及x86 CPU的L1/L2/L3缓存的情况下写入/读取内存?
是否完全由硬件管理的x86 CPU缓存?
编辑:我想这样做,因为我想采样内存的速度,看看内存的任何部分性能是否下降.
当从连续的内存位置执行一系列_mm_stream_load_si128()调用(MOVNTDQA)时,硬件预取器是否仍会启动,或者我应该使用显式软件预取(使用NTA提示)以获得预取的好处,同时仍然避免缓存污染?
我问这个的原因是因为他们的目标似乎与我相矛盾.流加载将获取绕过缓存的数据,而预取器尝试主动将数据提取到缓存中.
当顺序迭代一个大型数据结构(处理过的数据不会在很长一段时间内被修饰)时,我有必要避免污染chache层次结构,但我不想因频繁出现频繁的~100次循环处罚-fetcher闲置.
目标架构是Intel SandyBridge
是否有任何此类处理器具有绕过特定数据缓存的指令?这个问题也有一个答案,表明SSE4.2指令绕过缓存.有人可以启发我吗?
我正在阅读 MDS 攻击论文RIDL:Rogue In-Flight Data Load。他们讨论了 Line Fill Buffer 如何导致数据泄漏。有关于 RIDL 漏洞和负载的“重放”问题讨论了漏洞利用的微架构细节。
阅读该问题后,我不清楚的一件事是,如果我们已经有了存储缓冲区,为什么还需要行填充缓冲区。
John McCalpin 在WC-buffer 与LFB 有什么关系?中讨论了存储缓冲区和行填充缓冲区是如何连接的?在英特尔论坛上,但这并没有真正让我更清楚。
对于存储到 WB 空间,存储数据将保留在存储缓冲区中,直到存储退出之后。退役后,数据可以写入 L1 数据缓存(如果该行存在且具有写入权限),否则会为存储未命中分配一个 LFB。LFB 最终会收到缓存行的“当前”副本,以便它可以安装在 L1 数据缓存中,并且可以将存储数据写入缓存。合并、缓冲、排序和“捷径”的细节尚不清楚......与上述合理一致的一种解释是 LFB 用作缓存行大小的缓冲区,其中存储数据在发送到L1 数据缓存。至少我认为这是有道理的,但我可能忘记了一些事情......
我最近才开始阅读乱序执行,所以请原谅我的无知。这是我关于商店如何通过商店缓冲区和行填充缓冲区的想法。
在x64上,如果您首先在短时间内在先前未缓存的地址上写入完整缓存行的内容,然后在再次从该地址读取后不久,CPU是否可以避免必须从内存读取该地址的旧内容?
同样有效的是,先前的内存内容并不重要,因为全部高速缓存行中的数据已被完全覆盖?我可以理解,如果这是对未缓存地址的部分缓存行写入,然后是读取,则将产生必须与主内存等同步的开销。
从文档方面看,写分配,写合并和监听使我对此事有些困惑。目前,我认为x64 CPU无法做到这一点?
我想将数据存储在一个大型数组中,并_mm256_stream_si256()在循环中调用.据我所知,然后需要一个内存栅栏来使这些更改对其他线程可见._mm_sfence()说的描述
对在此指令之前发出的所有存储器到存储器指令执行序列化操作.保证在程序顺序之前的每个商店指令在程序顺序之后的任何商店指令之前全局可见.
但是,我最近的当前线程存储是否也可以在后续加载指令中看到(在其他线程中)?或者我必须打电话_mm_mfence()?(后者似乎很慢)
更新:我之前看过这个问题:我什么时候应该使用_mm_sfence _mm_lfence和_mm_mfence.那里的答案主要集中在何时使用围栏.我的问题更具体,该问题的答案不太可能解决这个问题(目前不这样做).
UPDATE2:在注释/答案之后,让我们将"后续加载"定义为线程中的加载,该线程随后获取当前线程当前持有的锁.
我正在尝试测量内存的写入带宽,我创建了一个 8G 字符数组,并使用 128 个线程在其上调用 memset。下面是代码片段。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <pthread.h>
int64_t char_num = 8000000000;
int threads = 128;
int res_num = 62500000;
uint8_t* arr;
static inline double timespec_to_sec(struct timespec t)
{
return t.tv_sec * 1.0 + t.tv_nsec / 1000000000.0;
}
void* multithread_memset(void* val) {
int thread_id = *(int*)val;
memset(arr + (res_num * thread_id), 1, res_num);
return NULL;
}
void start_parallel()
{
int* thread_id = malloc(sizeof(int) * threads);
for (int i = 0; i < …Run Code Online (Sandbox Code Playgroud) 我知道现在很多编译器都非常擅长优化代码。但是,如果一个完全理解现代pc架构的人,是否有可能使代码比编译器更快?比如,如果他用 100% 的汇编编写代码,专注于架构呢?如果它确实有所作为,是否值得?