0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

if-else的效率有多低你知道吗?

Q4MP_gh_c472c21 来源:程序喵大人 作者:程序喵大人 2021-02-02 10:30 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

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

// 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 《《 “

”; std::cout 《《 “sum = ” 《《 sum 《《 “

”; } return 0;}~/test$ g++ test_predict.cc ;。/a.out7.95312sum = 480124300000

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

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

结果为:

~/test$ g++ test_predict.cc ;。/a.out24.2188sum = 480124300000

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

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

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

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

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

6d2c97d0-624f-11eb-8b86-12bb97331649.png

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

取指:Fetch

译指:Decode

执行:execute

回写: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 《《 “

”; std::cout 《《 “sum = ” 《《 sum 《《 “

”; } return 0;}

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

6da42ab6-624f-11eb-8b86-12bb97331649.png

图片截自下方参考资料中

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

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

拓展阅读:

《虚函数真的就那么慢吗?它的开销究竟在哪里?来看这4段代码!》

想必很多人都听说过虚函数开销大,貌似很多答案都说是因为虚函数表导致的那一次间接调用,真的如此吗?

直接看下面这两段代码:

#include 《cmath》#include “timer.h”struct Base { public: virtual int f(double i1, int i2) { return static_cast《int》(i1 * log(i1)) * i2; }};

int main() { TimerLog t(“timer”); Base *a = new Base(); int ai = 0; for (int i = 0; i 《 1000000000; i++) { ai += a-》f(i, 10); } cout 《《 ai 《《 endl;}

执行时间:12.895s

#include 《cmath》#include “timer.h”struct Base { public: int f(double i1, int i2) { return static_cast《int》(i1 * log(i1)) * i2; }};

int main() { TimerLog t(“timer”); Base *a = new Base(); int ai = 0; for (int i = 0; i 《 1000000000; i++) { ai += a-》f(i, 10); } cout 《《 ai 《《 endl;}

执行时间:12.706s

这两段代码的执行时间几乎没有区别,可见虚函数表导致的那一次函数间接调用并不浪费时间,所以虚函数的开销并不在重定向上,这一次重定向基本上不影响程序性能。

那它的开销究竟在哪里呢?看下面两段代码,这两段代码和上面相比只改动了一行:

#include 《cmath》#include “timer.h”struct Base { public: virtual int f(double i1, int i2) { return static_cast《int》(i1 * log(i1)) * i2; }};

int main() { TimerLog t(“timer”); Base *a = new Base(); int ai = 0; for (int i = 0; i 《 1000000000; i++) { ai += a-》f(10, i); // 这里有改动 } cout 《《 ai 《《 endl;}

执行时间:436ms

#include 《cmath》#include “timer.h”struct Base { public: int f(double i1, int i2) { return static_cast《int》(i1 * log(i1)) * i2; }};

int main() { TimerLog t(“timer”); Base *a = new Base(); int ai = 0; for (int i = 0; i 《 1000000000; i++) { ai += a-》f(10, i); // 这里有改动 } cout 《《 ai 《《 endl;}

执行时间154ms

这里看到,仅仅改变了一行代码,虚函数调用就比普通函数慢了几倍,为什么?

虚函数其实最主要的性能开销在于它阻碍了编译器内联函数和各种函数级别的优化,导致性能开销较大,在普通函数中log(10)会被优化掉,它就只会被计算一次,而如果使用虚函数,log(10)不会被编译器优化,它就会被计算多次。如果代码中使用了更多的虚函数,编译器能优化的代码就越少,性能就越低。

虚函数通常通过虚函数表来实现,在虚表中存储函数指针,实际调用时需要间接访问,这需要多一点时间。

然而这并不是虚函数速度慢的主要原因,真正原因是编译器在编译时通常并不知道它将要调用哪个函数,所以它不能被内联优化和其它很多优化,因此就会增加很多无意义的指令(准备寄存器、调用函数、保存状态等),而且如果虚函数有很多实现方法,那分支预测的成功率也会降低很多,分支预测错误也会导致程序性能下降。

如果你想要写出高性能代码并频繁的调用虚函数,注意如果用其它的方式(例如if-else、switch、函数指针等)来替换虚函数调用并不能根本解决问题,它还有可能会更慢,真正的问题不是虚函数,而是那些不必要的间接调用。

正常的函数调用:

复制栈上的一些寄存器,以允许被调用的函数使用这些寄存器;

将参数复制到预定义的位置,这样被调用的函数可以找到对应参数;

入栈返回地址;

跳转到函数的代码,这是一个编译时地址,因为编译器/链接器硬编码为二进制;

从预定义的位置获取返回值,并恢复想要使用的寄存器。

而虚函数调用与此完全相同,唯一的区别就是编译时不知道函数的地址,而是:

从对象中获取虚表指针,该指针指向一个函数指针数组,每个指针对应一个虚函数;

从虚表中获取正确的函数地址,放到寄存器中;

跳转到该寄存器中的地址,而不是跳转到一个硬编码的地址。

通常,使用虚函数没问题,它的性能开销也不大,而且虚函数在面向对象代码中有强大的作用。

但是不能无脑使用虚函数,特别是在性能至关重要的或者底层代码中,而且大项目中使用多态也会导致继承层次很混乱。

那么有什么好方法替代虚函数呢?这里提供几个思路,读者请持续关注,后续会具体讲解:

使用访问者模式来使类层次结构可扩展;

使用普通模板替代继承和虚函数;

C++20中的concepts用来替代面向对象代码;

使用variants替代虚函数或模板方法。

这几种方法是Michael Spertus大佬介绍的,各有各的优缺点,作者都会用,但什么情况下使用哪个,取决于你自己的判断,这里只是教你了一个工具,什么时候用都取决于你自己。

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

文章出处:【微信公众号:嵌入式ARM】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11218

    浏览量

    222977
  • 编程
    +关注

    关注

    90

    文章

    3708

    浏览量

    96768
  • 代码
    +关注

    关注

    30

    文章

    4941

    浏览量

    73158

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

文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    睿擎EtherCAT轴控制技术:如何实现抖动高精度运动控制 | 深度解析

    在工业自动化领域,运动控制的精度和稳定性直接决定了生产效率和产品质量。近日,睿擎SDKV1.5.0版本重磅发布,其中EtherCAT轴控制技术的升级尤为引人注目。今天,我们就来深入解析这项技术
    的头像 发表于 10-15 18:39 5381次阅读
    睿擎EtherCAT<b class='flag-5'>多</b>轴控制技术:如何实现<b class='flag-5'>低</b>抖动高精度运动控制 | 深度解析

    薄膜电容的关键词是什么知道吗

    薄膜电容是一种以金属箔作为电极,以聚乙酯、聚丙烯、聚苯乙烯等塑料薄膜作为电介质的电容器,在电子电路中具有重要作用。薄膜电容哪些关键词知道吗
    的头像 发表于 10-13 15:30 288次阅读
    薄膜电容的关键词是什么<b class='flag-5'>你</b><b class='flag-5'>知道吗</b>?

    知道板卡厂商参与芯片研发的α阶段意味着什么?

    大家都知道芯片很重要,但是否知道一颗芯片从设计构思到最终量产,需要经历怎样一个漫长的过程吗?
    的头像 发表于 09-24 17:08 7272次阅读
    <b class='flag-5'>你</b><b class='flag-5'>知道</b>板卡厂商参与芯片研发的α阶段意味着什么?

    LED亮度含义知道吗

    选购LED灯具或LED显示产品时,“亮度够不够”往往是人们考量的重要因素。
    的头像 发表于 09-23 17:42 1526次阅读

    想搞定控制器连接?耐达讯Modbus转Profinet这招知道吗

    点,还能提升整个系统的性能和效率。如果也在为协议连接问题发愁,不妨试试这个“破圈密码”。在项目中还有哪些协议连接难题呢?
    发表于 07-25 15:41

    已收藏!需要知道的57个常用树莓派命令!

    初次使用树莓派并不总是那么容易,因为可能还没有使用命令行的习惯。然而,终端命令是必不可少的,而且通常比通过图形用户界面(GUI)操作更高效。那么,哪些重要的命令是应该知道的呢?
    的头像 发表于 07-23 18:36 821次阅读
    已收藏!<b class='flag-5'>你</b>需要<b class='flag-5'>知道</b>的57个常用树莓派命令!

    薄膜电容器的优点哪些

    薄膜电容器虽然理论上有很多种材质,我们实际生产时主要有CBB金属化聚丙烯薄膜电容和CL金属化聚酯薄膜电容两种类型,它是电路上极重要的一类电子元器件,大部分电路都离不开它们,薄膜电容器的优点哪些,真的知道吗
    的头像 发表于 07-21 16:03 826次阅读

    知道船用变压器哪些吗?

    在船舶和海洋平台上,电力系统的稳定运行至关重要,而船用变压器作为其中的关键设备,其种类繁多,各具特点。知道船用变压器哪些吗?让我们一起来揭开它们的神秘面纱。CSD船用变压器是船舶供电系统中
    的头像 发表于 06-01 00:00 482次阅读
    <b class='flag-5'>你</b><b class='flag-5'>知道</b>船用变压器<b class='flag-5'>有</b>哪些吗?

    超低功耗蓝牙模组的功耗到底

    对于BLE(低功耗蓝牙)产品方案来说,功耗,决定着的产品待机、使用寿命是多久,或者是多久需要充一次电,推荐一个非常的好用的评估nordicBLE功耗的工具: 利用我们的这个工具
    发表于 05-27 22:37

    实测 PTR54LS05低功耗到底

    实测 PTR54LS05低功耗到底?
    发表于 04-27 10:57

    、中、高频段 (LMH) 模/频段功率放大器模块 skyworksinc

    电子发烧友网为提供()、中、高频段 (LMH) 模/频段功率放大器模块相关产品参数、数据手册,更有、中、高频段 (LMH)
    发表于 04-11 18:32
    <b class='flag-5'>低</b>、中、高频段 (LMH) <b class='flag-5'>多</b>模/<b class='flag-5'>多</b>频段功率放大器模块 skyworksinc

    非标超声波清洗机的优势,针对性的工件清洗方案

    通过定制化设计和超声波技术有效解决的零部件特殊构造所带来的清洗难题,更是最大限度的解决了人工效率的问题。要知道,工业制造领域,出厂速度是一家企业是都能获得订单的
    的头像 发表于 04-03 16:25 544次阅读
    非标超声波清洗机的优势,针对性的工件清洗方案

    串口服务器品牌排名背后,隐藏着的行业潜规则知道吗

    在科技飞速发展的当下,串口服务器作为连接串口设备与网络的重要桥梁,在工业、金融、交通等众多领域都有着广泛应用。市场上的串口服务器品牌繁多,各种品牌排名也让人眼花缭乱。但是否想过,这些排名背后可能
    的头像 发表于 03-27 13:09 487次阅读

    请问大神们这个集成块什么作用?代换型号吗?非常感谢!

    找了好久都没找到这个集成块的资料,哪位大神知道吗?用在功放机上的。谢谢啦!
    发表于 03-03 17:23

    LED户外显示屏的五大优势,知道吗

    LED户外显示屏的五大优势,知道吗? LED户外显示屏在城市的夜晚中扮演着越来越重要的角色,其鲜艳的色彩、生动的画面为城市增添了一抹亮色。那么,LED户外显示屏的显示效果到底如何呢?让我们一起
    的头像 发表于 01-06 18:20 1325次阅读