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

简介: 笔记

if-else涉及到分支预测的概念,这里详细讲解一下。

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

// 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,使用方式如图:



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

相关文章
|
3月前
|
存储 数据采集 算法
《花100块做个摸鱼小网站! 》第三篇—热搜表结构设计和热搜数据存储
本文档详细介绍了如何搭建并实现一个简易的热搜数据抓取及存储系统。首先,通过基础链接导航引导读者了解项目的组成部分,包括服务器配置、示例网站和源码库。接着,逐步讲解了数据库表结构设计、利用MyBatis插件自动生成Java对象的过程,以及热搜数据的存储逻辑。在数据存储部分,特别关注了唯一ID生成算法,确保数据不会重复。此外,还提供了爬取抖音和百度热搜的具体实现代码,包括使用OkHttp进行网络请求、利用Jsoup解析HTML文档等技术细节。通过本文档的学习,读者不仅能掌握热搜数据抓取与存储的基本方法,还能了解到一些实用的开发技巧和工具。
56 1
《花100块做个摸鱼小网站! 》第三篇—热搜表结构设计和热搜数据存储
|
4天前
|
Web App开发 前端开发 JavaScript
前端开发必备神器大公开,用过的人都哭了:效率翻倍不是梦!
前端开发结合了创意与技术,本文介绍了几个提升开发效率的工具:Visual Studio Code、Webpack、Postman、GitHub 和 Chrome DevTools。这些工具分别在代码编辑、模块打包、API 测试、版本控制和网页调试等方面发挥重要作用,帮助开发者提高工作效率,优化项目管理。
13 4
|
4月前
|
数据采集 前端开发 NoSQL
《花100块做个摸鱼小网站!· 序》灵感来源
# 序 大家好,我是summo。去年趁阿里云99元一年的2核2G服务器优惠,我买了一台,起初用于练手Linux和部署数据库等环境,后来决定搭建一个摸鱼小网站。受摸鱼网站启发,创建了[上班摸鱼](https://sbmy.fun),一个聚合热搜的网页。总花费109元(含10元域名),用两周摸鱼时间完成。虽未广泛推广,已有2万访问量。计划分享搭建过程,包括技术调研、爬虫编写等。一起动手,100元获得实操经验!]
91 1
《花100块做个摸鱼小网站!· 序》灵感来源
|
6月前
|
缓存 前端开发 搜索推荐
博客有点丑,魔改优化来一波🛠️
博客有点丑,魔改优化来一波🛠️
117 1
|
搜索推荐 小程序 程序员
看过很多教程,却依然写不好一个程序,怎么破?
最近在和学员的沟通中,发现不少初学者面临这样一个问题:了解了一些基本的语法,看得懂书上的示例,但是面临一个新的编程问题时,依然感到无从下手。
|
算法
零碎的算法笔记(1)
零碎的算法笔记(1)
72 0
|
SQL XML 前端开发
别再学了!这些技术已经被淘汰了,少走点弯路。。。
别再学了!这些技术已经被淘汰了,少走点弯路。。。
|
Web App开发 存储 前端开发
【番外01】吐血整理5万字100道高频基础面试题 无名面试集《烂俗前端》
【番外01】吐血整理5万字100道高频基础面试题 无名面试集《烂俗前端》
196 0
|
SQL 存储 Oracle
平时做开发需要掌握哪些数据库方面的知识(个人经验之谈)
平时做开发需要掌握哪些数据库方面的知识(个人经验之谈)
238 0
|
前端开发 JavaScript
当下做前端开发,不算简单,这篇文章可以让少走很多弯路以及需要掌握的知识
当下做前端开发,不算简单,这篇文章可以让少走很多弯路以及需要掌握的知识