多线程是否真的有必要?

2023-05-16

一点疑问

       相比大家在投简历、面试等等过程中,或多或少会遇到这么一个问题:熟悉掌握多线程开发;谈谈你对多线程的认识。

       其实,我有这么一个疑问,那就是多线程真的有必要么?根据我这两年来的项目经验,也或多或少用了一些多线程的东西,其中有的失败了,有的成功了。但是根据我的所知,有很多程序虽然计算规模很大,实际上却没有使用多线程,但是速度依然很快。有很多程序就算用了多线程,也不见得快了多少。

       那么,多线程真的有必要么?

 

一个例子

       由于我对多线程的了解也是非常浅薄,不敢断言说“必要”或者“不必要”。由于暂定学习多线程定在第三季度或者第四季度,所以仅发表一些个人的观点。

       曾经看过这么一段代码,从中来浅析一下一个程序性能瓶颈是否在于“多线程”。例子如下:

int main(void)
{
   const size_t arraySize = 30000;
   int data[arraySize];
   for (size_t c = 0; c < arraySize; ++c)
    {
       data[c] = std::rand() % 256;
    }
 
   clock_t start = clock();
   long long sum = 0;
 
   for (size_t i = 0; i < 100000; ++i)
    {
       for (size_t c = 0; c < arraySize; ++c)
       {
           if (data[c] >= 128)
           {
                sum += data[c];
           }
       }
    }
 
   double elapsedTime = static_cast<double>(clock() - start) /CLOCKS_PER_SEC;
   std::cout << elapsedTime << std::endl;
   std::cout << "sum = " << sum << std::endl;
 
   return 0;
}

       考虑这个程序,简单说就是从3万个随机数中找到大于等于128的进行累加,这个过程循环10万次,在Visual Studio 2013,Win32平台Release下执行时间为:17s。

那么可以进行优化么?乍看起来,好像无从下手,这么简单的代码还需要进行优化吗?

 

第一种优化

       既然提到了优化,那么必然会有方法了;既然这里提到第一种优化,那么必然会有第二种方法了。

       先上代码,代码不多加,不少加,只有一行。

int main(void)
{
   const size_t arraySize = 30000;
   int data[arraySize];
   for (size_t c = 0; c < arraySize; ++c)
    {
       data[c] = std::rand() % 256;
    }
   std::sort(data, data + arraySize);
 
   clock_t start = clock();
   long long sum = 0;
 
   for (size_t i = 0; i < 100000; ++i)
    {
       for (size_t c = 0; c < arraySize; ++c)
       {
           if (data[c] >= 128)
           {
                sum += data[c];
            }
       }
    }
 
   double elapsedTime = static_cast<double>(clock() - start) /CLOCKS_PER_SEC;
   std::cout << elapsedTime << std::endl;
   std::cout << "sum = " << sum << std::endl;
 
   return 0;
}

        如上,只加了一行排序。现在的执行时间为:4.339s。是不是会觉得不可思议,是不是要问一个为什么。

        如果你读过计算机操作系统,或者说计算机体系结构等一类的教科书,解释这个问题是很简单的,就是Cache命中率问题。

        相比大家接触编程语言时,老师一般会提问这么一个问题:一个二维数组遍历时,应该先横向遍历还是先竖向遍历?

        其实这两个问题说起来本质是一样的。计算机读取内存,首先先将一块内存映射到Cache中,然后从Cache里读取;然后读取下一块内存,如果还在Cache中,那么就直接从Cache中读取来提高速度。那么如果数组进行了排序,相当于读取下一个数据,还是在Cache中的,不需要再读取内存,这样就大幅度减少了内存反复映射的问题,提高了Cache命中率。仅仅这么短小的代码,效率居然差了4倍左右。(结论错误,请参考补充部分)


第二种优化

       第一种优化通过提高Cache的命中率来进行优化,那么第二种就不会从相同角度来考虑了。发动你的大脑,如何解决?

       我们说到代码优化,一般都是通过减少循环嵌套程度,减少分支语句来进行处理。那么现在减少循环是做不到了,接下来就是考虑分支语句问题了。

       分支语句只有if (data[c] >= 128),不卖关子了,这句代码从计算机的角度来考虑,就是以下写法:

int main(void)
{
   const size_t arraySize = 30000;
   int data[arraySize];
   for (size_t c = 0; c < arraySize; ++c)
    {
       data[c] = std::rand() % 256;
    }
 
   clock_t start = clock();
   long long sum = 0;
 
   for (size_t i = 0; i < 100000; ++i)
    {
        for (size_t c = 0; c < arraySize; ++c)
       {
           int val = (data[c] - 128) >> 31;
           sum += (~val & data[c]);
       }
    }
 
   double elapsedTime = static_cast<double>(clock() - start) /CLOCKS_PER_SEC;
   std::cout << elapsedTime << std::endl;
   std::cout << "sum = " << sum << std::endl;
 
   return 0;
}

       如果你不懂上面的写法,也许你需要了解一下计算机反码,补码等表示方式,那么就可以明白为什么这么写了,这里就不再赘述了。

       换做这种方式,时间消耗为:7.416s。虽说比第一种慢了不少,但是远比原始代码快的多。可见分支语句也是降低程序效率的一大关键之一,整天强调降低圈复杂度不是全无道理的。


再说多线程

       通过这两种优化,主要是第一种优化,我想再次去说明多线程这个问题。如果多线程涉及到数据共享问题,那么Cache命中率这个是否会被列入主题之内呢?纵然多线程更好的利用了CPU的资源,但是真的高效么?我没有实际的经验,不敢多说什么,但是直觉上考虑,可能真的没有那么高效。如果再涉及到虚拟缓存,可能就真的不是那么高效了。

       另外,我也是只想问一句,面试官们,你们真的懂多线程吗?你们真的用了多线程了吗?用了多线程真的高效了吗?

       C++不是技术的大话,请你们仔细思考尝试后,再去提问这些奇葩的问题,有些东西并不是看起来高大尚就是真正的高大尚!


进一步思考

       在《如何阅读一本书》里面学到这个技巧,真是受益匪浅,发散思维,考虑更多更多的东西,在这个环节中我也学习了更多。

       第一,正如第一种优化方式,其实某些代码看起来很简单,但是并没有达到它真正的作用,这种代码大家都会随心所欲的写出来。既然这么简单的代码,我们都会“写错”,那么真正复杂的逻辑呢?一定要慎重啊。

       第二,第二种优化方式,告诉了我们其实有很多方式可以做更好的替换,虽然这种代码不宜读,不容易理解,但是某些核心的部分,只有这么一丁点的改变,就可以提升2倍的效率,代码优化的过程可以给我们帮助,相关资料可以参考《短码之美》,感谢宁哥一直以来的支持和帮助。

       第三,回到主题,多线程其实不简单,不要觉得就那么几个函数,加锁,创建线程,解锁就OK了。如果这么浮躁的对待多线程,这将是你未来路上的一个陷阱,很可能跌进去就爬不出来,在这点上我已有深刻的体会。

       第四,时刻不要忘记计算机的根基,操作系统和体系结构,只学语言,不学平台,不去深入,那么语言永远只是语言,就像我们学了那么多年的英语,其实也只是一门语言,跟实际的交流,呵呵,差得远了去了。


补充

       多谢小李同学提出的观点:第一种优化前,分支预测成功率50%,优化后,分支预测100%,两种情况下的penalty导致了速度差别。

       首先,分支预测详细信息可以参考维基百科中分支预测器部分资料,地址:http://zh.wikipedia.org/wiki/%E5%88%86%E6%94%AF%E9%A0%90%E6%B8%AC。

那么假设分支预测失败,会有指令回退的penalty,大约损失10-20个时钟周期。对于我的CPU(Intel P7450,2.13GHz),一个时钟周期1000/2.13GHz = 0.47纳秒。按照50%的方式计算,10个时钟周期损失计算,总时间大约为:

        100000 * 30000 *0.5 * 10 * 0.47 = 7.05s

        如果是20个时钟周期则为14.1s的损耗,可以推断我上述关于Cache命中率的结论不是主要原因所在。

        那么测试一下Cache命中率的情况,测试代码如下:

int main(void)
{
   const size_t arraySize = 300;
   int data[arraySize][arraySize];
   for (size_t i = 0; i < arraySize; ++i)
    {
       for (size_t j = 0; j < arraySize; ++j)
       {
           data[i][j] = std::rand() % 256;
       }
    }
 
   clock_t start = clock();
   long long sum = 0;
   for (size_t i = 0; i < 30000; ++i)
    {
       for (size_t i = 0; i < arraySize; ++i)
       {
           for (size_t j = 0; j < arraySize; j++)
           {
                sum += data[i][j];
           }
       }
    }
 
   double elapsedTime = static_cast<double>(clock() - start) /CLOCKS_PER_SEC;
   std::cout << elapsedTime << std::endl;
   std::cout << "sum = " << sum << std::endl;
 
   sum = 0;
   start = clock();
   for (size_t i = 0; i < 30000; ++i)
    {
       for (size_t j = 0; j <arraySize; j++)
       {
           for (size_t i = 0; i < arraySize; ++i)
           {
                sum += data[i][j];
           }
       }
    }
 
   elapsedTime = static_cast<double>(clock() - start) /CLOCKS_PER_SEC;
    std::cout << elapsedTime <<std::endl;
   std::cout << "sum = " << sum << std::endl;
 
   return 0;
}

       一共执行30000 * 300 * 300 = 2700000000次,与上面3 * 10^9基本接近,两次执行时间分别为4.734s和5.046s。

可见Cache的命中率其实不是主要的原因,我想主要是Cache足够大,应付这样的连续存储的数字应该无压力吧。当时考虑到这方面主要还是针对多线程的一些方面,真是有点风马牛不相及了。

       真如《你的知识需要管理》中提到一样,知识只有真正拿出来分享,才能发现问题所在,感谢大家的阅读,感谢小李的指正,Perfect!
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多线程是否真的有必要? 的相关文章

随机推荐

  • 转载:Snorkel - 可编程的数据标注神器

    我知道你已经用上了最先进的深度学习模型 xff0c 不过 xff0c 还在人工标注数据吗 xff1f 这有点过时了 xff01 快来了解下Snorkel 最新的基于弱监督学习的大规模训练数据标注神器 现在的机器学习尤其是深度学习模型很强大
  • ubuntu16.04使用阿路比-LPMS-IG1进行ros数据发布

    官方资料下载 https www alubi cn support download 这个是ros包下载地址 https bitbucket org lpresearch openzenros src master 这个包比较坑 xff0c
  • C++总结1-vector的指针

    在使用vector的时候犯了一个导致编译错误 xff0c 在这里记录一下 vector是非常常见也非常好用的容器 xff0c 但是vector的指针有一些特殊 如果想要获得vector的数组的指针 xff0c 我找到了两种方法 amp ve
  • Unity项目关闭Debug.Log没有Log

    接手的项目打包后没有记录自定义Log 卧槽 xff0c 这我怎么debug 搜索了一堆都只有说怎么关闭 xff0c 没什么怎么打开的 以至于我明明加了Andrid关键词还有说设置Use Player Log的 换了个关键词搜索怎么关闭 xf
  • 发行商提供证书打包iOS时 遇到的若干问题

    No signing certificate ios Development found No ios Development signing certificate matching 开发和发布总共收到了2个 p12文件 xff0c 另外
  • RPGMakerMV接入Greenworks,以支持steamworks API

    RPGMaker MV的基本原理 在查看Greenworks的github页面时有提到NW JS xff0c 实际上RPGMakerMV部署好的工程就是基于NW JS运行的 在根目录下的Game exe实际上可以用网上下载的NW exe替代
  • MBP合上盖子后仍反复自动唤醒

    现象 每天早上起来电脑都有点温度 xff0c 明明一直合着盖子但总是隔了几天就没电了 在办公室的时候开着steam就看到提示家里的笔记本可以远程流传输 通过休眠命令查看 xff0c 几乎每10分钟就有一条唤醒记录 大部分唤醒理由是 xff0
  • MAC 关闭office软件自动更新提示 (Microsoft AutoUpdate)

    参考 xff1a https blog csdn net weixin 42873928 article details 115936349 sudo chmod 000 Microsoft AutoUpdate app 执行的功能是设置文
  • git 出现 “fatal: The remote end hung up unexpectedly“

    情况 xff1a 有台两年没开的电脑长期没有更新git等工具版本 xff0c clone跟checkout的时候都有报这个问题 实际上没注意到git lfs filter process git lfs command not found这
  • VSCode智能补全代码片段技巧

    小技巧 foreach的代码片段中没有快速建议智能建议不优先推荐代码片段 xff08 试过也不大行 xff09 foreach的代码片段中没有快速建议 通过快速建议输入一个foreach之类的代码片段 xff0c 保持tab键可以切换输入位
  • Windows下搭建局域网内简易git服务器

    这里写自定义目录标题 概述配置步骤1 任意位置创建git 仓库2 启动Git Daemon3 其他电脑克隆工程4 开机自动启动5 其他配置注意事项 概述 由于和朋友小规模制作项目 xff0c 又使用了UE5这样的庞然大物 xff0c 准备整
  • 如果OpenStack给虚机自动分配的ip和其他静态配置的ip重复了怎么办

    1 查找你要修改ip地址的网卡id root 64 node 1 neutron port list 2 允许ip地址为10 10 1 56通过 root 64 node 1 neutron port update 4e79200f ac5
  • Lisp笔记

    变量 动态变量 defvar defparameter span class token punctuation span span class token car dafvar span paraname default value sp
  • MSDK接入 中的各种问题

    检查顺序 包名注意一下 Unity报 Found plugins with same names Found plugins with same names Assets Msdk BuglyPlugins Android libs bug
  • C++ Windows 窗体程序入门 - 1.你的第亿个窗体程序

    前言 43 学Windows窗体已经有一段时日了 xff0c 奈何没有什么浅显易懂 amp 便宜 xff01 xff01 的书籍 就想来 算是记笔记吧 顺便还能给你们总结一些经验 注 有许多内容源于我看过的一些视频 比如Chili和Cher
  • CSS替换元素和非替换元素

    根据是否可以通过修改某个属性值更改元素呈现的内容 xff0c 可以分为替换元素和非替换元素 替换元素 以下元素都是可替换元素 xff0c 以及在各种浏览器下的默认display值 xff08 图片来源 CSS世界 张鑫旭 xff09 针对
  • SD-WAN加速保障跨国公司数据传输质量

    很多企业开启国际化业务 xff0c 跨国文件传输越来越频繁 xff0c 而且随着业务的开展 xff0c 公司规模的扩张 xff0c 很多企业都在海外设置了分支机构 不得不说 xff0c 随着经济一体化的进程不断加快 xff0c 企业跨国经营
  • 零基础视觉SLAM(一)

    文章目录 SLAM简介什么是SLAM xff1f 传感器VSLAM架构视觉里程计后端优化 SLAM应用自学参考书预备知识 SLAM简介 什么是SLAM xff1f SLAM从本质上来说它要实现的就是通过传感器去实时地估计自身位置及经过的轨迹
  • 关于Proxmox 5.x的国内有效镜像源

    官网的 http download proxmox com 有多慢我就不提了 xff0c 否则大家也不会看到这篇小文 首先需要分清楚Proxmox VE的镜像构成 1 xff09 Debian自身 这个用国内哪个镜像都可以 xff0c al
  • 多线程是否真的有必要?

    一点疑问 相比大家在投简历 面试等等过程中 xff0c 或多或少会遇到这么一个问题 xff1a 熟悉掌握多线程开发 xff1b 谈谈你对多线程的认识 其实 xff0c 我有这么一个疑问 xff0c 那就是多线程真的有必要么 xff1f 根据