少写点if-else吧,它的效率有多低你知道吗?

# 干了这碗鸡汤

我要再和生活死磕几年。要么我就毁灭,要么我就注定铸就辉煌。如果有一天,你发现我在平庸面前低了头,请向我开炮。

--杰克·凯鲁亚克

if-else涉及到分支预测的概念,关于分支预测上篇文章《虚函数真的就那么慢吗?它的开销究竟在哪里?来看这4段代码!》程序喵就粗略提到过,这里详细讲解一下。

首先看一段经典的代码,并统计它的执行时间:

// test_predict.cc
#include <algorithm>
#include <ctime>
#include <iostream>
int main() {const unsigned ARRAY_SIZE = 50000;int data[ARRAY_SIZE];const unsigned DATA_STRIDE = 256;for (unsigned c = 0; c < ARRAY_SIZE; ++c) data[c] = std::rand() % DATA_STRIDE;std::sort(data, data + ARRAY_SIZE);{  // 测试部分clock_t start = clock();long long sum = 0;for (unsigned i = 0; i < 100000; ++i) {for (unsigned c = 0; c < ARRAY_SIZE; ++c) {if (data[c] >= 128) sum += data[c];}}double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;std::cout << elapsedTime << "\n";std::cout << "sum = " << sum << "\n";}return 0;
}
~/test$ g++ test_predict.cc ;./a.out
7.95312
sum = 480124300000

此程序的执行时间是7.9秒,如果把排序那一行代码注释掉,即

// std::sort(data, data + ARRAY_SIZE);

结果为:

~/test$ g++ test_predict.cc ;./a.out
24.2188
sum = 480124300000

改动后的程序执行时间变为了24秒。

其实只改动了一行代码,程序执行时间却有3倍的差距,而且看上去数组是否排序与程序执行速度貌似没什么关系,这里面其实涉及到CPU分支预测的知识点。

提到分支预测,首先要介绍一个概念:流水线。

拿理发举例,小理发店一般都是一个人工作,一个人洗剪吹一肩挑,而大理发店分工明确,洗剪吹都有特定的员工,第一个人在剪发的时候,第二个人就可以洗头了,第一个人剪发结束吹头发的时候,第二个人可以去剪发,第三个人就可以去洗头了,极大的提高了效率。

这里的洗剪吹就相当于是三级流水线,在CPU架构中也有流水线的概念,如图:

在执行指令的时候一般有以下几个过程:

  1. 取指:Fetch

  2. 译指:Decode

  3. 执行:execute

  4. 回写:Write-back

流水线架构可以更好的压榨流水线上的四个员工,让他们不停的工作,使指令执行的效率更高。

再谈分支预测,举个经典的例子:

火车高速行驶的过程中遇到前方有个岔路口,假设火车内没有任何通讯手段,那火车就需要在岔路口前停下,下车询问别人应该选择哪条路走,弄清楚路线后后再重新启动火车继续行驶。高速行驶的火车慢速停下,再重新启动后加速,可以想象这个过程浪费了多少时间。

有个办法,火车在遇到岔路口前可以猜一条路线,到路口时直接选择这条路行驶,如果经过多个岔路口,每次做出选择时都能选择正确的路口行驶,这样火车一路上都不需要减速,速度自然非常快。但如果火车开过头才发现走错路了,就需要倒车回到岔路口,选择正确的路口继续行驶,速度自然下降很多。所以预测的成功率非常重要,因为预测失败的代价较高,预测成功则一帆风顺。

计算机的分支预测就如同火车行驶中遇到了岔路口,预测成功则程序的执行效率大幅提高,预测失败程序的执行效率则大幅下降。

CPU都是多级流水线架构运行,如果分支预测成功,很多指令都提前进入流水线流程中,则流水线中指令运行的非常顺畅,而如果分支预测失败,则需要清空流水线中的那些预测出来的指令,重新加载正确的指令到流水线中执行,然而现代CPU的流水线级数非常长,分支预测失败会损失10-20个左右的时钟周期,因此对于复杂的流水线,好的分支预测方法非常重要。

预测方法主要分为静态分支预测和动态分支预测:

静态分支预测:听名字就知道,该策略不依赖执行环境,编译器在编译时就已经对各个分支做好了预测。

动态分支预测:即运行时预测,CPU会根据分支被选择的历史纪录进行预测,如果最近多次都走了这个路口,那CPU做出预测时会优先考虑这个路口。

tips:这里只是简单的介绍了分支预测的方法,更多的分支预测方法资料大家可关注公众号回复分支预测关键字领取。

了解了分支预测的概念,我们回到最开始的问题,为什么同一个程序,排序和不排序的执行速度相差那么多。

因为程序中有个if条件判断,对于不排序的程序,数据散乱分布,CPU进行分支预测比较困难,预测失败的频率较高,每次失败都会浪费10-20个时钟周期,影响程序运行的效率。而对于排序后的数据,CPU根据历史记录比较好判断即将走哪个分支,大概前一半的数据都不会进入if分支,后一半的数据都会进入if分支,预测的成功率非常高,所以程序运行速度很快。

如何解决此问题?总体思路肯定是在程序中尽量减少分支的判断,方法肯定是具体问题具体分析了,对于该示例程序,这里提供两个思路削减if分支。

方法一:使用位操作:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

方法二:使用表结构:

#include <algorithm>
#include <ctime>
#include <iostream>int main() {const unsigned ARRAY_SIZE = 50000;int data[ARRAY_SIZE];const unsigned DATA_STRIDE = 256;for (unsigned c = 0; c < ARRAY_SIZE; ++c) data[c] = std::rand() % DATA_STRIDE;int lookup[DATA_STRIDE];for (unsigned c = 0; c < DATA_STRIDE; ++c) {lookup[c] = (c >= 128) ? c : 0;}std::sort(data, data + ARRAY_SIZE);{  // 测试部分clock_t start = clock();long long sum = 0;for (unsigned i = 0; i < 100000; ++i) {for (unsigned c = 0; c < ARRAY_SIZE; ++c) {// if (data[c] >= 128) sum += data[c];sum += lookup[data[c]];}}double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;std::cout << elapsedTime << "\n";std::cout << "sum = " << sum << "\n";}return 0;
}

其实Linux中有一些工具可以检测出分支预测成功的次数,有valgrind和perf,使用方式如图:

图片截自下方参考资料中

条件分支的使用会影响程序执行的效率,我们平时开发过程中应该尽可能减少在程序中随意使用过多的分支,能避免则避免。

更多的分支预测方法资料大家可关注公众号回复分支预测关键字领取。

参考资料

http://matt33.com/2020/04/16/cpu-branch-predictor/

https://zhuanlan.zhihu.com/p/22469702

https://en.wikipedia.org/wiki/Branch_predictor

https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/466517.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js实现连接的两种放法

第一种用document.write输出 <html> <body> <script type"text/javascript"> var rMath.random() if (r>0.5) { document.write("<a hrefhttp://www.w3school.com.cn>学习 Web 开发&#xff01;</a>") } else { documen…

异或求校验和

uint8_t chk_xrl(const void *data, uint16_t length) {const uint8_t *buf data;uint8_t retval 0;while(length){retval ^ *buf;--length;}return retval; }

c语言笔试

1、局部变量能否和全局变量重名&#xff1f;   答&#xff1a;能&#xff0c;局部会屏蔽全局。要用全局变量&#xff0c;需要使用"::" ;局部变量可以与全局变量同名&#xff0c;在函数内引用这个变量时&#xff0c;会用到同名的局部变量&#xff0c;而不会用到全局…

聚宝盆,只要你上网就可以挣钱

点此注册[url]http://www.56cash.com/ref.php?id5429[/url]转载于:https://blog.51cto.com/435178/100909

为什么不能在中断上半部休眠?

这是一个老生常谈的问题。我们先简单说下什么是中断「因为最近在群里看到有人竟然不懂什么是中断」。中断是计算机里面非常核心的东西&#xff0c;我们可以跑OS&#xff0c;可以多任务运行都因为中断的存在。假设你是一个CPU&#xff0c;你正在睡觉。你突然觉得肚子疼&#xff…

打CALL APP 项目进展 总体计划

时间进展完成度参与人员备注2018.3完成app的前端设计 全体 2018.4app的后端 2018.5app的后端 转载于:https://www.cnblogs.com/aliceluorong/p/8520442.html

单片机中通用的类型别名

单片机中通用的类型别名 #ifndef _TYPE_H_ #define _TYPE_H_#ifdef __GNUC__ #define __packed __attribute__((aligned(1))) #endif/* exact-width signed integer types */ typedef signed char int8_t; typedef signed short int int16_t; typedef sign…

j.u.c系列(08)---之并发工具类:CountDownLatch

写在前面 CountDownLatch所描述的是”在完成一组正在其他线程中执行的操作之前&#xff0c;它允许一个或多个线程一直等待“&#xff1a;用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法&#xff0c;所以在当前计数到达零之前&#xff0c;await 方法会一直受…

巧用1个GPIO控制2个LED显示4种状态

很多电子产品有状态指示灯&#xff0c;比如电视机&#xff1a;待机状态亮红灯开机状态亮绿灯实现起来很简单&#xff0c;微控制器MCU的两个GPIO分别控制就行&#xff1a;不过资源总是紧张的&#xff0c;有时候会碰到GPIO不够用的情况。如果只用1个GPIO&#xff0c;可不可以实现…

GetTickcount函数

GetTickCount是一种函数。GetTickCount返回&#xff08;retrieve&#xff09;从操作系统启动所经过&#xff08;elapsed&#xff09;的毫秒数&#xff0c;它的返回值是DWORD。 GetTickcount函数&#xff1a;它返回从操作系统启动到当前所经过的毫秒数&#xff0c;常常用来判断某…

网络大小端转换函数

网络大小端转换函数 //***************************************************************************** // // htonl/ntohl - big endian/little endian byte swapping macros for // 32-bit (long) values // //**********************************************************…

5-全排列总结:

https://www.nowcoder.com/acm/contest/76/H 给一道题&#xff0c;可以去测试代码。 这里总结一下全排列的几种方法&#xff1a; 方法一&#xff1a;利用交换排列&#xff1a;缺点&#xff1a;不能按字典序排列&#xff0c;但可以借助set处理。 #include <bits/stdc.h> …

大大大大数怎么求余?C语言

问题&#xff1a;一个特别大的数除以23求余数用C语言应该怎么算啊&#xff1f;比如23232323232323232323232323232323232323232323232323232323233除以23&#xff0c;怎么算余数&#xff1f;数据类型在计算机的存储是有大小限制的&#xff0c;所以才出现了大数求余这种问题&…

substr

substr &#xff08;C语言函数&#xff09; 编辑 substr是C语言函数&#xff0c;主要功能是复制子字符串&#xff0c;要求从指定位置开始&#xff0c;并具有指定的长度。如果没有指定长度_Count或_Count_Off超出了源字符串的长度&#xff0c;则子字符串将延续到源字符串的结尾…

***站长自述挂马经历 提醒挂马者回头是岸

我做站都已经接近三年了&#xff0c;期间像很多人一样买过很多玉米&#xff0c;但是因为养不起&#xff0c;至今只保留了一个域名&#xff08;159e.cn&#xff09; &#xff0c;当时学校正流行移动159的号码&#xff0c;然后e在网络上代表很多意思&#xff0c;就注册了这个域名…

微信公众号--相关资料

相关资料 l 官方文档&#xff1a;https://mp.weixin.qq.com/wiki?tresource/res_main&idmp1445241432 l 测试号&#xff1a;https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?actionshowinfo&tsandbox/index l 接口调试地址&#xff1a;https://mp.weixin.qq.…

程序员因拒绝带电脑回家工作被开除!获赔19.4万元

近日&#xff0c;男子拒绝春节带电脑回家工作被开除的消息&#xff0c;成为了不少网友关注的焦点&#xff0c;引发网友共鸣。因为春节拒绝带工作电脑回家被开除&#xff0c;上海一位软件工程师起诉公司获赔19.4万元。2月2日&#xff0c;据上海浦东法院公众号消息&#xff0c;该…

键值键名

在注册表中 所谓的键&#xff0c;是指一个注册表条目。 键名&#xff0c;是这个条目的名称 键值是为这个条目所赋予的值。 比如&#xff1a; NoDesktop1 这就是一个键 NoDesktop是一个键名 1就是一个键值 这个条目的意思是说&#xff1a;NoDesktop是没有桌面当值为1的时候&…

随便写写(5)

也许是今年发生的事情太多了&#xff0c;所以比以前要更关注时事&#xff0c;虽然面对一些既成的事实&#xff0c;难免要进行痛心的思考。 昨天晚上关注了一下东方卫视播出的9.8特大尾矿库溃坝事故的后续报道&#xff0c;这起特大人为事故已经得到了认定&#xff0c;相关的责任…

STM32 LED灯的另一种写法

STM32 LED灯的另一种写法 #ifndef __BSP_LED_ #define __BSP_LED_#include <MM32x103.h> // 这个换成STM32的库文件就行 #include "type.h"// #define LED1_RUN_GRP GPIOC #define LED1_RUN_IDX GPIO_Pin_6 #define LED1_RUN_OFF() GPIO_ResetBit…