以下是一些简单的优化措施,可将性能降低到CPU之外

Johannes PlenioUnsplash拍摄的照片

在作为软件开发人员的职业生涯开始之初,大多数人认为程序的性能归结为其操作的渐进复杂性。 但是,一旦您在其中应用了最佳算法,而您的代码仍无法提供理想的性能,您应该怎么做?

以下是一些基本技术,可以帮助您从CPU内核中获得额外的收益。

循环展开

现代的CPU内核在其执行单元中具有多个功能单元,例如ALU ,Load / Store,分支。

程序应始终尝试利用它来实现并行性。

一种技术是使用循环展开 。 循环展开允许您打破参数上的依赖链。 乱序的CPU可以并行执行多个指令,从而使程序运行更快。

 func F1 () int {
var acc int = 1;
for j := 0; j < length; j++ {
acc = acc + inputList1[j]
}
return acc
}
 func F2 () int {
var acc1 int = 1;
var acc2 int = 1;

for j := 0; j + 1 < length; j += 2 {
acc1 = acc1 + inputList1[j]
acc2 = acc2 + inputList1[j + 1]
}
return acc1 + acc2
}

在F1中,程序通常必须等待acc更新才能执行下一个循环。 但是,在F2中,您的程序将利用内核中的多个整数加法单元,并将并行进行。 如果运行基准测试,您会发现F2的速度几乎是F1的两倍。

矢量指令

现代CPU可以将向量(原始类型的数组)作为一个单元而不是单个元素进行操作。 向量指令涉及在单个指令中一次加载多个值并对其进行存储。 这些指令称为SIMD (单指令,多数据)。 SIMD的流行扩展是SSEAVX以及那里的扩展。

SIMD指令通常可以一次处理8个32位或4个64位值,因此可以使程序真正更快。 通常,编译器会注意这一点。

快取

缓存通常被程序员视为理所当然。 您编写的代码通常会对是否进行有效的缓存产生重大影响。

github为例

 func  ColumnTraverse() int {
var ctr int

for col := 0; col < cols ; col++ {
for row := 0; row < rows ; row++ {
if matrix[row][col] == 0xFF {
ctr++
}
}
}

return ctr
}

func RowTraverse() int {
var ctr int

for row := 0; row < rows ; row++ {
for col := 0; col < cols ; col++ {
if matrix[row][col] == 0xFF {
ctr++
}
}
}

return ctr
}

这两个函数都在做相同的事情,并且具有相同的渐近复杂度。 如果运行基准测试,您会发现RowTraverse通常比ColumnTraverse快4倍。

这是因为行遍历会利用缓存中已存在的值,但是ColumnTraverse必须在每次比较时从内存中获取一个值。

您可以在我的文章中阅读有关缓存的更多信息

分支预测

现代CPU在看到分支时不会停止(即条件类型)。 取而代之的是,他们假定是否采用了分支,并推测性地执行下一条指令。 仅在确定预测是否正确时才提交结果。 如果预测错误,CPU将丢弃结果,并在分支之后从第一条语句开始执行指令。 除了执行更多指令的额外开销外,这种遗漏通常会导致5-10 ns的损失。

如果您的程序在if-else之间切换很多,那么分支错误预测损失将对您的性能造成损害。 避免这种情况的最佳方法是使用尽可能少的分支,如果绝对必要,则使它们可预测(例如,在比较之前对值进行排序)。

并发!=并行

您将线程数从10增加到100,程序的性能变得更差。 为什么会这样?

问题是CPU。 它仅具有有限数量的内核来支持并行操作。 在应用程序中似乎并行发生时,处理器通常会在线程之间快速切换。 但是,这种切换非常昂贵,因为它涉及来回复制PC,SP和内存/高速缓存中的所有寄存器。

如果您有大量的线程,CPU将大部分时间花在这些线程之间,而不是执行实际的指令。 现代语言试图通过在几个OS线程上复用大量抽象(例如goroutine)来解决此问题。

互斥体

如果您实现了最佳的并发性,但是您正在使用锁来同步数据结构,那么您仍然不会注意到应有的性能提升。

这是因为简单的互斥锁锁定/解锁大约需要25ns。 这似乎并不多,但是一旦您以1000万RPM的速度运行程序,则25ns变为250ms,这是很多。

避免互斥的最简单方法之一就是拥有线程本地数据结构。 如果需要在线程之间共享数据,请使用无锁通信技术(例如goroutine中的通道),而不要使用共享内存DS。

内存参考

除非绝对需要,否则不应该使用内存引用。 编译器通常会避免对内存引用进行优化,因为它可能具有空前的含义。

 func  AddPointers(x *int, y *int){
*x += *y
*x += *y
}

一个简单的优化编译器应该做的是使它* x = 2 *(* x)+ * y从而减少指令数

但是,如果x和y指向相同的变量,则AddPointers将给出输出4(* x),而优化将给出输出3(* x),这是不正确的。 编译器在遇到这种情况时会安全地播放,并且会避免任何优化。

可以应用更多的技术,例如内核旁路,避免内核间通信等。但是这些技术不满足简单性的标准。 我将在另一篇文章中详细讨论这些内容。

涉及这些主题的一些资源包括:

LinkedIn Facebook 上与我联系, 发送邮件至 kharekartik@gmail.com 共享反馈。

From: https://hackernoon.com/here-are-some-simple-optimisations-to-squeeze-performance-out-of-cpu-6306342ed1a5

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值