当前位置 > 首页 > 时尚快讯 > 正文

4 大妙招,教你快速搞定复杂的系统编程!
  • 发布时间:2019-09-26
  • www.web918.com.cn
  • CSDN 2天前我想分享所谓的系统编程,顾名思义,指的是操作系统的代码编写,如Windows,Unix系统编程。与我们常见的应用程序编程不同,系统编程更接近硬件,它使用不同的函数库和库函数调用方法。那么,面对更复杂的系统编程,作为开发人员,开发人员是什么?有哪些更好的优化措施?

    作者|保罗卡瓦拉罗

    翻译|苏本如,编辑|涂敏

    制作| CSDN(ID:CSDNnews)

    以下是翻译:在本文中,我们将概述一些常用的优化技术和一些“系统编程”技巧。无论今天的“系统编程”是什么意思,我们都会介绍一些方法,使您的代码运行更快,更高效,并让您从您获得的任何知识中获得更多。本文中讨论的所有示例都可以从GitHub上的这个地方获得:paulcavallaro/systems-programming。缓存行和伪共享在现代对称多处理(SMP)系统中,“虚假共享”是一个非常容易理解的多线程代码优化问题。对这个问题的讨论非常广泛。一个基本思想是机器上的物理内存不是无限粒度的,也就是说,你不能只读取一个字节。相反,当您想要读取一个字节的内存时,处理器不仅会读入并缓存该字节,还会读取并缓存该字节周围的数据,因为它假定数据也可能被使用。这个读取和缓存的数据单元称为“缓存行”,实质上是可以访问的最小内存块。自2019年起,高速缓存行全部为2个幂,通常在32到256个字节之间,最常见的大小为64个字节。现在,为了支持计算机上的多个处理器以一致的方式从同一内存块读取和写入,此计算机上只有一个处理器必须具有对给定高速缓存行的独占访问权限。 “伪共享”意味着意外地将两个不相关的数据块放在同一个高速缓存行中。当两个处理器分别更新两个不同数据块中的数据(例如多个计数器的值)时,它们会相互干扰,因为每个处理器都试图以独占方式访问这两个数据。块的缓存行。对“伪共享”这个名称的解释是,虽然这两个计数器在理论上不应该相互影响,但它们没有充分理由“错误地共享”一个缓存行。一种解决方案是强制将数据写入单独的缓存行。在C/C ++语言中,这可以通过强制struct/class成员的对齐来完成。在这个示例examples/cache-lines.cc中,我们使用abseil(注意:Google的内部C ++代码库,现在是开源)宏ABSL_CACHELINE_ALIGNED来实现这一点。为了证明实际效果,我们在两个不同的结构体NormalCounters和CacheLineAwareCounters中对std:原子类型的计数器进行了基准测试。该基准分别测试了运行1,2,3和4个线程的情况。每个线程在结构中触发一个单独的原子计数器65,536次。以下是使用Haswell处理器在2013 MacBook Pro计算机上进行处理的结果:对上述结果的评论:时间表示每个线程从开始到结束的挂钟时间,CPU表示每个线程使用的CPU时间。线。我们可以看到两个结构的大小不同,其中:sizeof(NormalCounters)=64,sizeof(CacheLineAwareCounters)=256。这是因为我们对各个字段施加了对齐约束,以便每个成员都在其自己的缓存行上。因此,Int64不像往常一样占用8个字节,而是占用一个完整的缓存行,在我的机器上是64字节。我们还看到,对于单线程情况,NormalCounters和CacheLineWareCounters之间的性能差异很小。但是当我们添加更多线程时,CacheLine Aware Counters比易受伪共享错误影响的简单普通计数器执行得更好。有趣的是,CacheLine AwareCounters在单线程情况下需要比在多线程情况下更长的挂钟时间,这可能指向微妙的基准测试问题或者可能具有固定的延迟量,但在多线程情况下,这种延迟分散在多个线程,因此每个线程的延迟量看起来更小。 Magic 2乘法器(功率)是当前硬件中最昂贵的操作之一,其中昂贵意味着“最长的延迟”。 Agner Fog的指令延迟列表列出了Intel的Skylake处理器的DVI指令在两个64位寄存器上运行,延迟时间为35-88个周期,而ADD指令在相同的两个64位寄存器上运行,延迟时间仅为一个周期。因此,在其他操作可以完成相同的工作的地方,我们应该尽量避免使用除法运算。除了实际划分之外,最常见的操作之一是模块化操作(%)。模块化操作的常见位置是哈希表:要从哈希表传输到存储桶,需要模块化操作,例如HASH%TABLE_SIZE。模块化操作更常用的另一个领域是开放寻址算法,因为我们需要不断地将值重新映射回哈希表桶空间。那么模块化操作如何帮助我们从哈希表转移到桶?这有点无聊但惊人的乘数2!首先,让我揭示答案:我们将强制所有哈希表为大小为2的N次幂。我们可以使用此功能来代替更快的位紊乱。此外,此功能易于维护。每当我们需要增加散列表的大小来分摊重新散列的成本时,我们会将散列表的大小加倍,因此当散列表增长时,它们的大小将保持为2的幂。现在,我们使用除法或模块化操作来将哈希值映射到哈希表中的桶索引。存储区索引必须严格小于哈希表的大小,并且此映射的哈希值应处于无序状态。为了不使用除法运算,我们将使用位掩码来“屏蔽”除严格小于2次幂之外的所有设置。这样,所有熵都可以保持在最低位,就像模块化操作一样,但速度要快得多。 Agner Fog将此操作放在指令列表中,在同一英特尔Skylake架构中有一个周期延迟。作为对比特小说以及如何选择位掩码的简要回顾,让我们来看看位模式。因为数字以二进制表示,所以我们知道2的每个幂(数字N)只有一个比特集。例如,这意味着所有N-1值都比log2(N)的有效位低一位。例如,为了在HASH%N计算中替换我们的模运算符,我们使用按位AND运算来计算HASH&的值。 (N-1)。这将仅保留低于我们的log_2(N)位的设置,将任何HASH值映射到[0,N]之间的数字。如有必要,我们甚至可以缓存位掩码,以便以后不必重新计算。为了证明使用位掩码技术比使用普通模块化操作更快,我编写了一个小基准来比较一百万个模块化操作和一百万个位掩码操作的结果。从上面的测试结果我们可以看出,使用模运算符执行DIV指令比使用“位掩码”慢约28倍,这比预测的Agby Fog慢35倍。由于这种技术很容易实现并且提供了一个很好的示例,因此它已被许多高性能哈希表使用,例如abseil Swiss Tables的flat_hash_set和flat_hash_map,以及ConcurrencyKit的ck_ht_map。地址位使用的调整通常,您希望在指针上存储一个或两个附加消息。事实上,这种做法非常普遍,维基百科有一篇专门讨论它的文章。实现此目的的一种方法是利用许多64位系统(例如Linux)上的虚拟内存地址空间仅为48位的事实,尽管我们使用8个字节来存储它们。这意味着我们可以将我们想要的任何旧东西放在前16位,当我们真的不想引用它时,我们可以掩盖它。下面是一些C ++代码示例,它们使用指针的最高位来存储底层数据是否“脏”。但是,有趣的是,由于这是Linux内存管理/虚拟地址空间的一个特性,它可能会发生变化并实际发生变化! LWN(Linux Weekly News)于2017年发布了一个补丁集,它实现了一个五级页表,以支持更多的可寻址内存空间。如果启用此更改,Linux的虚拟内存寻址空间将从当前的48位增加到57位,将虚拟内存寻址空间的大小从256 TiB增加到128 PiB,这对每个人都足够了。默认情况下无法启用此更改。部分原因是各种高性能程序,尤其是各种JavaScript引擎和LuaJIT,其中对高端寻址空间使用的调整导致一些额外的数据被打包到指针中。当您希望多个线程专门访问共享数据时,锁定条带锁可用于互斥。缺点是如果频繁访问共享数据,并且这是系统的关键部分,那么线程可能将大部分时间花在锁争用而不是实际工作上。解决此问题的常用方法是引入更多锁。你说什么?等待!好吧,我想说的是:不是一个保护所有数据的锁,而是一些只负责部分数据的锁。通过这种方式,我们将数据划分为单独的非竞争性存储桶。假设数据访问方法趋于一致,增加数据分段将按比例减少争用锁的线程数。以下是用C ++编写的一个小示例,它提供了两个线程安全哈希集的实现。 ThreadSafeHashSet的第一个实现使用单个锁来保护单个基本哈希集(absl: flat_hash_set)。 LockStripedHashSet的第二个实现具有N个单独的锁,用于保护N个单独的基本哈希集(abs: flat_hash_sets)。为了说明锁定条带化的好处,我们在存在多个线程的情况下对两个线程安全的哈希集性能进行了基准测试,每个线程都插入了一百万个条目。对于LockStripedHashSet,我们尝试将数据拆分为4个和8个块。结果如下:类似地,Time表示每个线程的挂钟时间,CPU表示每个线程使用的CPU时间。另请注意,由于我的计算机只有4个逻辑核心,因此此测试最多只能运行4个线程,因为此范围之外的任何线程实际上都不会导致任何其他争用。从上面我们可以看到,在单线程的情况下,LockStripedHashSet比简单的ThreadSafeHashSet略差,无论是阻塞还是非阻塞,挂钟和CPU时间。但是,随着线程数量的增加,锁的争用增加,并且LockStripedHashSet在这种情况下执行得更好。在线程数较多的情况下,将数据拆分为8个块比拆分为4个块要好。虽然锁定条带可以帮助减轻对锁的争用,但是它具有增加锁的存储开销的缺点。在我们的示例中,对于我们的一个基准测试,7个额外锁定和额外absl: flat_hash_set簿记的开销很小,但是如果在应用程序中使用8向条带,则线程安全散列集将替换所有这些散列集,所以你可能会增加你的内存使用量。结论尽管上面的内容远不是最常见的系统编程技术的详尽列表,但我希望它能激励您学习更多知识,掌握更多工具来提高自己应用程序的性能,或者至少它会让您更加容易。理解为什么对性能敏感的代码正在做它正在做的事情。原文:本文是针对CSDN的翻译,请注明出处。

    [结束]

    热门文字推荐

    你看到的每一点,“我在看,”我认真地成为报告和报告投诉的最爱

    所谓的系统编程,顾名思义,是指操作系统的代码编写,如Windows,Unix系统编程。与我们常见的应用程序编程不同,系统编程更接近硬件,它使用不同的函数库和库函数调用方法。那么,面对更复杂的系统编程,作为开发人员,开发人员是什么?有哪些更好的优化措施?

    作者|保罗卡瓦拉罗

    翻译|苏本如,编辑|涂敏

    制作| CSDN(ID:CSDNnews)

    以下是翻译:在本文中,我们将概述一些常用的优化技术和一些“系统编程”技巧。无论今天的“系统编程”是什么意思,我们都会介绍一些方法,使您的代码运行更快,更高效,并让您从您获得的任何知识中获得更多。本文中讨论的所有示例都可以从GitHub上的这个地方获得:paulcavallaro/systems-programming。缓存行和伪共享在现代对称多处理(SMP)系统中,“虚假共享”是一个非常容易理解的多线程代码优化问题。对这个问题的讨论非常广泛。一个基本思想是机器上的物理内存不是无限粒度的,也就是说,你不能只读取一个字节。相反,当您想要读取一个字节的内存时,处理器不仅会读入并缓存该字节,还会读取并缓存该字节周围的数据,因为它假定数据也可能被使用。这个读取和缓存的数据单元称为“缓存行”,实质上是可以访问的最小内存块。自2019年起,高速缓存行全部为2个幂,通常在32到256个字节之间,最常见的大小为64个字节。现在,为了支持计算机上的多个处理器以一致的方式从同一内存块读取和写入,此计算机上只有一个处理器必须具有对给定高速缓存行的独占访问权限。 “伪共享”意味着意外地将两个不相关的数据块放在同一个高速缓存行中。当两个处理器分别更新两个不同数据块中的数据(例如多个计数器的值)时,它们会相互干扰,因为每个处理器都试图以独占方式访问这两个数据。块的缓存行。对“伪共享”这个名称的解释是,虽然这两个计数器在理论上不应该相互影响,但它们没有充分理由“错误地共享”一个缓存行。一种解决方案是强制将数据写入单独的缓存行。在C/C ++语言中,这可以通过强制struct/class成员的对齐来完成。在这个示例examples/cache-lines.cc中,我们使用abseil(注意:Google的内部C ++代码库,现在是开源)宏ABSL_CACHELINE_ALIGNED来实现这一点。为了证明实际效果,我们在两个不同的结构体NormalCounters和CacheLineAwareCounters中对std:原子类型的计数器进行了基准测试。该基准分别测试了运行1,2,3和4个线程的情况。每个线程在结构中触发一个单独的原子计数器65,536次。以下是使用Haswell处理器在2013 MacBook Pro计算机上进行处理的结果:对上述结果的评论:时间表示每个线程从开始到结束的挂钟时间,CPU表示每个线程使用的CPU时间。线。我们可以看到两个结构的大小不同,其中:sizeof(NormalCounters)=64,sizeof(CacheLineAwareCounters)=256。这是因为我们对各个字段施加了对齐约束,以便每个成员都在其自己的缓存行上。因此,Int64不像往常一样占用8个字节,而是占用一个完整的缓存行,在我的机器上是64字节。我们还看到,对于单线程情况,NormalCounters和CacheLineWareCounters之间的性能差异很小。但是,当我们添加更多线程时,cachelineawerecounter的性能要比容易出现“伪共享”错误的简单普通计数器好得多。有趣的是,在单线程情况下,cachelinewarecounters需要比多线程情况下更长的挂钟时间,这可能会导致一些微妙的基准测试问题,或者可能具有固定的延迟量,但在多线程处理中,这个延迟量分布在多个线程上,因此每个线程的延迟量看起来更小。magic 2(power)在当前硬件中,除法是最昂贵的操作之一,这里的昂贵意味着“最长延迟”。agner fog的指令延迟列表列出了英特尔skylake处理器在两个64位寄存器上运行的div指令,延迟为35-88个周期以及在相同的两个64位寄存器上运行add指令的延迟。只有一个周期。因此,在其他行动可以做同样工作的地方,我们应该尽量避免使用分部行动。除实际除法外,除法运算的一个常见位置是模运算(%)。模块操作的一个常见位置是哈希表:要从哈希表移动到bucket,需要执行一个模块操作,例如哈希%table\u size。模块操作的另一个更常用的地方是开放寻址算法,因为我们需要不断地将值重新映射回哈希表存储桶空间。那么,模运算如何帮助我们从散列表移到桶中呢?这将谈到一个有点无聊,但惊人的2力量!首先,让我揭示答案:我们将强制所有哈希表具有2n次方的大小。我们可以使用这个特性来替换除法运算,使其更快速地旋转。此外,此功能易于维护。每当我们需要增加散列表的大小来分摊重新散列的成本时,我们会将散列表的大小加倍,因此当散列表增长时,它们的大小将保持为2的幂。现在,我们使用除法或模块化操作来将哈希值映射到哈希表中的桶索引。存储区索引必须严格小于哈希表的大小,并且此映射的哈希值应处于无序状态。为了不使用除法运算,我们将使用位掩码来“屏蔽”除严格小于2次幂之外的所有设置。这样,所有熵都可以保持在最低位,就像模块化操作一样,但速度要快得多。 Agner Fog将此操作放在指令列表中,在同一英特尔Skylake架构中有一个周期延迟。作为对比特小说以及如何选择位掩码的简要回顾,让我们来看看位模式。因为数字以二进制表示,所以我们知道2的每个幂(数字N)只有一个比特集。例如,这意味着所有N-1值都比log2(N)的有效位低一位。例如,为了在HASH%N计算中替换我们的模运算符,我们使用按位AND运算来计算HASH&的值。 (N-1)。这将仅保留低于我们的log_2(N)位的设置,将任何HASH值映射到[0,N]之间的数字。如有必要,我们甚至可以缓存位掩码,以便以后不必重新计算。为了证明使用位掩码技术比使用普通模块化操作更快,我编写了一个小基准来比较一百万个模块化操作和一百万个位掩码操作的结果。从上面的测试结果我们可以看出,使用模运算符执行DIV指令比使用“位掩码”慢约28倍,这比预测的Agby Fog慢35倍。由于这种技术很容易实现并且提供了一个很好的示例,因此它已被许多高性能哈希表使用,例如abseil Swiss Tables的flat_hash_set和flat_hash_map,以及ConcurrencyKit的ck_ht_map。地址位使用的调整通常,您希望在指针上存储一个或两个附加消息。事实上,这种做法非常普遍,维基百科有一篇专门讨论它的文章。实现此目的的一种方法是利用许多64位系统(例如Linux)上的虚拟内存地址空间仅为48位的事实,尽管我们使用8个字节来存储它们。这意味着我们可以将我们想要的任何旧东西放在前16位,当我们真的不想引用它时,我们可以掩盖它。下面是一些C ++代码示例,它们使用指针的最高位来存储底层数据是否“脏”。但是,有趣的是,由于这是Linux内存管理/虚拟地址空间的一个特性,它可能会发生变化并实际发生变化! LWN(Linux Weekly News)于2017年发布了一个补丁集,它实现了一个五级页表,以支持更多的可寻址内存空间。如果启用此更改,Linux的虚拟内存寻址空间将从当前的48位增加到57位,将虚拟内存寻址空间的大小从256 TiB增加到128 PiB,这对每个人都足够了。默认情况下无法启用此更改。部分原因是各种高性能程序,尤其是各种JavaScript引擎和LuaJIT,其中对高端寻址空间使用的调整导致一些额外的数据被打包到指针中。当您希望多个线程专门访问共享数据时,锁定条带锁可用于互斥。缺点是如果频繁访问共享数据,并且这是系统的关键部分,那么线程可能将大部分时间花在锁争用而不是实际工作上。解决此问题的常用方法是引入更多锁。你说什么?等待!好吧,我想说的是:不是一个保护所有数据的锁,而是一些只负责部分数据的锁。通过这种方式,我们将数据划分为单独的非竞争性存储桶。假设数据访问方法趋于一致,增加数据分段将按比例减少争用锁的线程数。以下是用C ++编写的一个小示例,它提供了两个线程安全哈希集的实现。 ThreadSafeHashSet的第一个实现使用单个锁来保护单个基本哈希集(absl: flat_hash_set)。 LockStripedHashSet的第二个实现具有N个单独的锁,用于保护N个单独的基本哈希集(abs: flat_hash_sets)。为了说明锁定条带化的好处,我们在存在多个线程的情况下对两个线程安全的哈希集性能进行了基准测试,每个线程都插入了一百万个条目。对于LockStripedHashSet,我们尝试将数据拆分为4个和8个块。结果如下:类似地,Time表示每个线程的挂钟时间,CPU表示每个线程使用的CPU时间。另请注意,由于我的计算机只有四个逻辑核心,因此该测试最多只能运行四个线程,因为超出此范围的任何线程实际上都不会导致任何额外的争用。从上面可以看出,在单线程的情况下,LockStripedHashSet的时钟和CPU时间性能略差于简单的ThreadSafeHashSet。但是,随着线程数量的增加以及锁争用的增加,LockStripedHashSet在这种情况下表现更好。当线程数高时,最好将数据分成8个块而不是将其分成4个块。尽管锁定条带化可以帮助减少锁定的争用,但其缺点是它增加了锁定的存储开销。在我们的示例中,对于我们的一个基准测试,七个附加锁和额外的absl: flat_hash_set簿记的开销很小,但如果在应用程序中用八向条带线程安全散列集替换所有这些散列集,你可能会使其内存使用量大大增加。虽然结论远不是最常见的系统编程技术的详细列表,但希望它能激发您学习更多知识的愿望,掌握更多工具来提高您自己的应用程序的性能,或者至少让您更容易理解为什么性能敏感的代码正在做它正在做的事情。爱。原文:本文是为CSDN翻译的,请注明出处。

    [结束]

    热门文字推荐

    你看到的每一点,“我在看,”我认真地成为了一个最喜欢的

    日期归档

    沈丘资讯网 版权所有© www.web918.com.cn 技术支持:沈丘资讯网 | 网站地图