并行算法与性能优化读书笔记

第一章 绪论

  • 并行:parallelism,是指在具有多个处理单元的系统上,通过将计算或数据划分为多个部分,将各个部分分配到不同的处理单元上,各处理单元相互协作,同时运行,以达到加快求解速度或者提高求解问题规模的目的。
  • 并发:concurrency,并发是指在一个处理单元上运行多个应用,各应用分时占用处理单元,是一种微观上串行、宏观上并行的模式,有时也称其为时间上串行、空间上并行
  • 代码性能优化:是指通过调整源代码,使得其生成的机器指令能够更高效地执行,通常高效是指执行时间更少、使用的存储器更少或能够计算更大规模的问题
  • 提高硬件性能(3种方式):让处理器一个周期处理多条指令;使用向量指令;在同一个芯片中集成多个处理单元
  • 向量化是指使用同一条指令同时操作多个数据
  • 多核技术是采用在同一个芯片上集成多个核心 的办法
  • 进程:对操作系统正在运行的程序的一种抽象,提供大粒度并行,进程可运行在多核上,线程只能运行在一个核上。进程和程序有关系也有不同,程序是静态指令的集合,进程是程序正在运行的状态。 进程是资源拥有的独立单位,不同的进程拥有不同的虚拟地址空间,不能够直接访问其他 进程的上下文资源。 ·
  • 进程上下文切换:系统内的多个进程通过时间片轮转并发执行,进程在时间片到期时保存正在运行的进程的状态,并运行另 一个等待运行的进程。 上下文是指保持进程运行所需要的寄存器、缓存和DRAM等资源。在任何一个无限精 度的时刻,一个处理器核心最多只能运行一个进程或一个线程,当操作系统需要在某个核心上运行另一个进程时,就会进行上下文切换。 由于进程的上下文切换和通信比较耗时,因此基于进程的并发往往适合于大 粒度的任务并行。
  • 进程之中可以有许多线程,这些线程共享进程的上下文,如虚拟地址空间和文件,但 是独立执行且可通过存储器进行通信。当进程终止时,进程内的所有线程也会同时终止。 另外线程也有其私有逻辑寄存器、栈和指令指针PC。
  • 混合或超级并行:在节点间使 用进程级并行,在节点内的多核上使用线程级并行,这称为混合或超级并行。
  • 超线程:超线程技术将一个物理处理器核模拟成两个逻辑核,可并行执行两个线程,能够在单 个时针周期内在两个线程间切换,让单核都能使用线程级并行计算,减少了CPU的闲置时 间,提高CPU的运行效率。 超线程通过 双倍增加一些资源(PC和寄存器)来减少线程的切换代价
  • 多核的每个核心里面具有独立的一级缓存,共享的或独立的二级缓存,有些机器还有 独立或共享的三级/四级缓存,所有核心共享内存DRAM。通常第一级缓存是多核处理器 的一个核心独享的,而最后一级缓存(Last Level Cache,LLC)是多核处理器的所有核心 共享的,大多数多核处理器的中间各层也是独享的。
  • 超线性加速:多核处理器的每个核心都有独立的一级缓存,有时还有独立的二级缓存,使用多 线程/多进程程序时可利用这些每个核心独享的缓存,这是超线性加速(指在多核处理器 上获得的性能收益超过核数)的原因之一。
  • UMA(均匀内存访问)和NUMA(非均匀 内存访问)。UMA是指多个核心访问内存中的任何一个位置的延迟是一样的,NUMA和 UMA相对,核心访问离其近(指访问时要经过的中间节点数量少)的内存其延迟要小

第二章 现代处理器特性

乱序执行处理器:现代处理器利用了指令级并行技术,同一时刻存在着多条指令同时执行,并且处理器执行指令的顺序无须和汇编代码给出的指令顺序 完全一致,编译器和处理器只需要保证最终结果一致,这类处理器称为“乱序执行处理 器”。

主流处理器减少访问数据延迟方法:利用程序的局部性特点,采用了一系列小而快的缓存以保存正在访问和将要被访问 的数据,以近似于内存的价格获得近似于缓存的速度; 利用并行性,在一个控制流由于高延迟的操作而阻塞时,执行另一个控制流

性能优化方式:

  • 1、指令级并行:
    • a) pipeline定义、“pipeline长度”的定义:流水线(pipeline)是一串操作的集合,其中前一个操作 的输出是下一个操作的输入。流水线执行允许在同一时钟周期内重叠执行多个指令。如ARM A9的指令流水线长度为8,而A15为13。 带有长流水线的处理器想要达到最佳性能,需要程序给出高度可预测的控制流。代码 主要在紧凑循环中执行的程序,可以提供恰当的控制流
    • b) 指令级乱序:乱序执行是指后一条指令比前一条指令先开始执行,但要求这两条指令 没有数据及控制依赖。通过重排缓冲区(ReOrder Buffer,ROB)实现,并且具有远多于逻辑寄存器数量的物理寄存器以支持寄存器重命名。
    • c)指令多发射:只要是具有多个执行单元的处理器,无论是否支持乱序执行,都要求其指令发射能力大于执行 能力。 指令发射单元一个周期内会发射多条指令。
    • d)分支预测:处理器遇上分支指令(判断指令,如if)的时,选择某条分支执行,一旦选择错误,处理器就需要丢弃已经执行的结果,且从正确 的分支开始执行 。显示分支预测,gcc中__builtin_expect
    • e)VLIW(Very Long Instruction Word):VLIW 的并行指令执行基于编译时已经确定好的调度
  • 2、向量化并行:
    • a)SIMD(单指令,多数据) :SIMD是指一条指令作用在多个数据上面,目前Intel X86提供了SSE/AVX指令,向量 寄存器长度分别为128位、256位。
    • b)SIMT(单指令,多线程)
  • 3、线程级并行:线程级并行的思想是: 1)在多核处理器上,使用多个线程使得多个核心同时执行多条流水线; 2)在单核处理器上,当某一个线程由于等待数据到达而空闲时,可以立刻导入其他 的就绪线程来运行。在单核处理器上使用多线程技术,使核心能够始终处于忙碌的状态
    • a)内核线程与用户线程:运行在用户空间(运行时)的线程称为用户线程,运行在内核空间(操作系统) 的线程称为内核线程。用户线程由库管理,无须操作系统支持,因此其创建、调度无须干 扰操作系统的运行,故消耗少。内核线程的创建、调度和销毁都由操作系统负责,操作系 统了解内核线程的运行情况。
    • b)多线程编程库:phread/OpenMP/OpenCL/ CUDA/OpenACC/ win32 thread;
    • c)多核上多线程并行要注意的问题: 线程过多(频繁上下文切换降低性能、污染缓存,通过线程数是物理核心的2倍)、数据竞争、死锁、饿死、伪共享
  • 4、缓存层次结构,包括缓存组织,缓存特点以及NUMA
    • 减少内存墙方式:每次内存访问、读取周围的多个数据,因为这些数据随后极有可能会被用到; 使用缓存;支持向量访问和同时处理多个访问请求,通过大量并行访问来掩盖延迟。 局部性分为“时间局部性”和“空间局部性”,时间 局部性是指当前被访问的数据随后有可能访问到;空间局部性是指当前访问地址附近的地 址可能随后被访问。现代处理器通过在内存和核心之间增加缓存以利用局部性增强程序性 能,这样可以用远低于缓存的价格换取接近缓存的速度。
    • 存储结构:自顶向下:CPU/寄存器——缓存——物理RAM(物理RAM/虚拟内存)——存储设备类型
    • 缓存一致性:保证缓存中的数据和内存中的数据是一致的。策略M(更改)E(排除)S(共享)I(无效)
    • 缓存不命中:冷不命中(程序启动时,缓存中无数据导致不命中)、满不命中(缓存被全部占用,原有的缓存x被覆盖)、冲突不命中
    • 写缓存:写回(指仅当一个缓存线需要被替换回内存时(缓存已满,或者多线 程时,其他线程需要访问这个数据),才将其内容写入内存或下一级缓存 )、写直达(write through),指每当缓存接收到写数据指令,都直接将数据写回到内 存或下一级缓存。
    • 写不命中会执行:写分配是指,如果要写的数据没有被缓存,那么就在缓 存中分配一条缓存线,这类似于大多数处理器对读的处理。 写不分配是指,如果要写的数据没有缓存,那么数 据就直接写入内存,而不占用缓存线,这有点类似于越过缓存
    • 缓存结构:缓存线、缓存组
    • 映射策略:直接相联、组相联和全相联
    • UMA (Uniform Memory Access, 一致性内存访问):总线模型保证了 CPU 的所有内存访问都是一致的,不必考虑不同内存地址之间的差异。在 UMA 架构下,CPU 和内存之间的通信全部都要通过前端总线
    • NUMA(Non-Uniform Memory Access,非一致性内存访问):numa是一种关于多个cpu如何访问内存的架构模型 。这种构架下,不同的内存器件和CPU核心从属不同的 Node,每个 Node 都有自己的集成内存控制器(IMC,Integrated Memory Controller)。在 Node 内部,架构类似SMP,使用 IMC Bus 进行不同核心间的通信;不同的 Node 间通过QPI(Quick Path Interconnect)进行通信 。NUMA框架下访问不同内存有远近之分,访问“靠近”处理器自身的存储器速度要比访问“远”的存储器快

img

  • NUMA相关命令行:numactl

    numactl --hardware #查看numa绑定情况,node distances表示几个node间的相对距离
    numactl --show     #查看你当前的NUMA设置
    
  • Linux的NUMA策略:Linux 识别到 NUMA 架构后,默认的内存分配方案是:优先从本地分配内存。如果本地内存不足,优先淘汰本地内存中无用的内存。使内存页尽可能地和调用线程处在同一个 node。

第三章 算法性能和程序性能的度量与分析

  1. 程序性能度量标准

    1. 时间复杂度的缺陷:1)只关注计算对性能的影响不关注访存;2)只关注最高阶计算量对性能的影响,而忽略低阶计算量对算法性能的影响; 3)只关注运算量的阶,而忽略阶的比例常数对算法性能的影响; 4)不关注不同的计算对性能的影响。
    2. 空间复杂度的缺陷:也忽略低阶和常数的影响
    3. 实现复杂度:程序的实际性能基本上由运行时的计算、访存和指令决定。计算复杂度、访存复杂度和指令复杂度统称实现复杂度。对于具体的代码而言,依据在某种处理器上运行的性能瓶颈不同而采用实现复杂度的 不同方面来度量。对于一个计算限制的算法,应当使用计算复杂度和指令复杂度;而对于 一个存储器限制的算法,应当使用访存复杂度来分析。
      1. 计算复杂度:计算复杂度考虑了程序中不 同的计算对性能的影响; 可用来衡量每一个控制流的计算数量,不但计算加载存储指令,还需要考 虑指令的吞吐量或延迟,通过加权平均来表示计算性能。
      2. 访存复杂度:访存复杂度考虑了访存对性能的影响 ,通常使用缓存层次的带宽来表示 ,计算访存复杂度时,由于缓存容量大小、替换策略和缓存线大小导致的额外开销也应 当计算在内。
      3. 指令复杂度:访存复杂度考虑了访存对性能的影响 。则分析实现算法的某个具体程序生成 的指令数量、类型、是否可以同时执行,通常使用执行代码关键路径所需要的时钟周期数来表示指令复杂度。
  2. 程序和指令的性能度量标准:

    1. 时间:计时的方法很多,如标准C库的time(与1970到现在的秒数)、clock系列函数(程序启动到现在的秒数),CUDA的事件,Linux下的 gettimeofday,Windows下的GetTickCount等 ;
    2. FLOPS:表示硬件每秒执行的浮点运算的数量,通常使用的单位有M(10 6)、 G(10 9)
    3. CPI(Clock Per Iteration):表示每次循环耗费的时钟周 期数,能够表示每次循环的平均代价。循环的CPI通常由其依赖链(指整个循环中相互依赖的运算或访存,也称为关键路 径)决定,减少循环CPI的过程就是减小或去掉循环内依赖链的过程
    4. 延迟:是指一条指令、一个操作从开始发射到完全结束所经历的时间。通常说一条指令 需要几个周期执行完成,这个周期数就是延迟。现代CPU采用的延迟优化技术有:乱序执行、缓存和多线程等
    5. 吞吐量:是指在单位时间内,硬件能够完成的操作数;对于访存来说, 吞吐量表示在单位时间内,硬件能够读写的字节数量。通常有两种方式表示吞吐量:①每 时钟周期指令数量(IPC);②每秒执行指令数量,对于浮点数据就意味着FLOPS。
    6. IPC(Instruction Per Clock)表示每时钟周期执行的指令数目,它是指令吞吐量的一 种具体表示。对于某个具体的程序来说,处理器的运行频率和指令的IPC决定了其性能。 处理器执行某种指令的吞吐量(FLOPS)的计算公式是:指令吞吐量=IPC×处理器频率。
  3. 程序性能优化的度量标准

    1. 加速比:来定义性能优化效果,其表示优化前程序的运行时间与优化后运行时间的比值
    2. 并行效率:定义为加速 比与计算单元核数的比例。并行效率一般要0.75以上
    3. 超线性加速:有时并行化获得的加速比会大于处理器数目。超线性加速的原 因之一是缓存,因为每个核心通常有自己的一级缓存,如果单个处理器没有办法将数据全 部放到缓存,那么多个处理器划分后的数据有可能放入缓存中;如果缓存的效果超越多线 程的开销时,那么就会出现超线性加速现象。
    4. Amdahl定律指出了在固定问题规模的前提下,增加处理器数量对程序整体性能的 影响。
    5. Gustafson定律指出了在固定每个处理器问题规模的前提下,增加处理器数量对 程序整体性能的影响
  4. 性能分析工具:

    1. gprof:是GNU编译器工具包提供的性能分析工具,gprof能够给出函数调用关系、调用次数、执行时间等信息。 在使用gcc编译程序时,添加选项-pg–g(pg表示产生的程序可以使用gprof分析) ,gprof通过在编译时插入代码来分析程序

      gcc x.c -pg -o x
      ./x
      gprof mytest gmon.out > result.txt
      
    2. valgrind:不用重新编译程序,可直接分析可执行文件,当然编译时增加-g选项能够提供更多的 信息。

      valgrind --tool=cachegrind 程序名 程序参数
      
    3. nvprof :用于分析运行在其GPU上的CUDA程序性能的工具

    4. vampire程序的目的却是消除并行程序中的性能“吸血鬼”。vampireTrace是一个基于命令行的并行程序剖分工具,而vampire能够图形化地显示 vampireTrace的结果。 vampire程序的目的却是消除并行程序中的性能“吸血 鬼”。vampireTrace是一个基于命令行的并行程序剖分工具,而vampire能够图形化地显示 vampireTrace的结果。

    5. Intel VTune工具能够分析程序的性能瓶颈、函数的调用关系、函数和语句的执行时 间、每条高级语言代码对应的汇编代码,以及并发性和锁的消耗。

    6. perf是Linux内核2.6.31之后支持的、用户空间的(Linux内核分用户空间和内核空间, 用户只能访问用户空间的数据、工具)、基于命令行的性能分析工具。perf提供了一系列 子命令。perf能够统计剖分整个Linux系统。

    # 全局性概况:
     perf list #查看当前系统支持的性能事件;
     perf bench #对系统性能进行摸底;
     perf test #对系统进行健全性测试;
     perf stat #对全局性能进行统计;
    # 全局细节:
    perf top  #可以实时查看当前系统进程函数占用率情况;
    perf probe  #可以自定义动态事件;
    # 特定功能分析:
    perf kmem  #针对slab子系统性能分析;
    perf kvm  #针对kvm虚拟化分析;
    perf lock  #分析锁性能;
    perf mem  #分析内存slab性能;
    perf sched  #分析内核调度器性能;
    perf trace  #记录系统调用轨迹;
    # 最常用功能perf record,可以系统全局,也可以具体到某个进程,更甚具体到某一进程某一事件;可宏观,也可以很微观。
    pref record  #记录信息到perf.data;
    perf report  #生成报告;
    perf diff  #对两个记录进行diff;
    perf evlist  #列出记录的性能事件;
    perf annotate  #显示perf.data函数代码;
    perf archive  #将相关符号打包,方便在其它机器进行分析;
    perf script  #将perf.data输出可读性文本;
    # 可视化工具perf timechart
    perf timechart record  #记录事件;
    perf timechart  #生成output.svg文档;
    

第四章 串行代码性能优化

分以下层次

  1. 系统级别:要求找出程序的性能控制因素(cpu?访存?网络?)以做针对性的优化。 常见因素有:

    (1)CPU利用率:采用top查看,常见有cpu空闲多,cpu占用率高,优化方式:优化运算,减少锁、优化分支等

    (2)存储器带宽利用率高:优化方式:提升局部性增加缓存利用、将数据保存到临时变量(会存到L1缓存或寄存器)减少存储器读写、减少读写依赖提升流水线效率、同时读写多个数据提高系统指令级并行和向量化能力

    (3)网络速度、利用率、负载均衡。优化:优化网线网卡、网络拓扑

    (4)去阻塞处理器运算的因素:如IO操作、其他进程抢占等。优化:使用非阻塞的函数调用、使用独立线程处理IO操作

  2. 应用级别:在程序编写前就要确定应用级别的配置,应用级别的优化相对比较简单,且实现后性能比较稳定。

    (1)编译器选项:gcc中编译选项O0(不编译优化)-O1-O2-O3(极致优化)、指定处理器架构、是否使用循环展 开、是否使用SSE指令。建议编译选项

    -O3(编译优化) -ffast-math(对超越函数使用更快版本) -funroll-all-loops(循环展开) -mavx(avx指令集向量化) -mtune=native(为当前编译的处理器做优化)
    

    (2)调用高性能库

    (3)去掉全局变量:全局变量会阻碍编译器的优化,最好规避

    (4)受限的指针:表示该指针不会和其他指针存在存储器别名,添加restrict。

    (5)条件编译:满足可移植性场景,使用条件编译(#if #else #endif)替换分支判断,条件编译生成代码更短,效率更高

  3. 算法级别:选择不同的数据组织方式,或者选择不同的算法,这两者对性能的作用非常大。 主要为了缓存优化:

    (1)索引顺序:考虑访问的局部性,如二维数组要按行访问

    (2)缓存分块:

    (3)软件预取:数据使用前通过prefetch预取到缓存中,需考虑实际和预取大小,不要过早预取,通常预取2~4个缓存线长度(x+256)的数据

    (4)查表法:把数据组织成表格,先进行预计算使用时直接进行查表,减少计算

  4. 函数级别:函数级别的优化通常用来减少函数调用的开销或者减少函数调用带来的编译器性能优化阻碍。 函数调用时,需要将调用参数通过寄存器或栈传递,且将函数返回地址入栈。函数级别的优化通常用于减少这部分的消耗及其导致的优化障碍。 建议函数只访问自己的局部变量和通过参数传入的值 。

    (1)函数调用参数:在64位X86处理器上,函数的参数优先通过寄存器传递,超量之后才会通过栈传递。 如果函数的参数是大结构体或类,应当通过传指针或引用以减少调用时复制和返回时 销毁的开销。

    (2)内联小函数:内联(inline)小函数(10行内)能消除函数调用的开销,提供更多的指令级并行、 表达式移除等优化机会,进而增加指令流水线的性能。 另外函数调用也可能阻止编译器优化,如果循环内有函数调用,则编译器很难进行向量化,循环中的函数调用尽量采用内联

  5. 循环级别:由于执行次数多,循环更易成为性能瓶颈,通常循环级别的优化能够发掘循环的并行性,减少循环内多余的运算,减少寄存器缓存的使用

    (1) 循环展开:一个循环内做多步操作。展开循环能减少每次的判断数量和循环变量改变的计算次数,增加处理器 流水线执行的性能。通常展开小循环且内部没有判断的。展开大循环则可能会因为导致寄存器溢出而导致性能下降,而展开内部有判断的循环会增加分支预测的开销也可能 会导致性能下降。 对于二层循环来说,通常建议优先展开外层循环。

    (2)循环累积:循环累积主要和循环展开同时使用,在减少寄存器的使用量的同时保证平行度。 即减少循环内临时变量的使用

    (3)循环合并:如果多个小循环使用的寄存器数量没有超过处理器的限制,那么合并这几个小循环可能会带来性能好处(增加了乱序执行能力)。循环合并可以减少判断次数,增加指令级并行能力(能够合并的两个循环内代码通常没有依赖)。

    (4)循环拆分:如果大循环(循环体内代码比较多、执行时间长)存在寄存器溢出的情况,那么将大 循环拆分为几个小循环,就能够提高寄存器的使用,而且能够为循环展开等技术的使用提供条件。 尽量使得拆分后的循环在使用资源上面不出现重复,即尽量不要因为 拆分循环导致访存或计算增加。

  6. 语句级别:不同的语句产生的指令数量并不相同,应尽量使用产生指令条数少的语句。 需要尽量避免语句生成不需要的指 令(生成更少数量的指令),或者让语句生成更加高效的指令(生成更快的指令)

    (1)减少内存读写:如果需要多次访问函数参数指针指向的值,应当先将其保存到寄存器中,可以使用临时变量减少内存的读写

    (2)选用尽量小的数据类型:

    (3)结构体对齐:声明结构体时,尽量大数据类型在前,小数据类型在后,一方面这样会节省一些空 间,另一方面可以更好地满足处理器的对齐要求。 可添加占位变量辅助对齐。

    (4)表达式移除:表达式移除是指去掉重复的、共同的计算或访存,这能够减少计算或内存访问次数。

    (5)优化交换性能:使用两个临时变量,减少读写之间的依赖关系,提升并行性。tmp1=a;tmp2=b;a=tmp2;b=tmp1;

    (6)分支优化:硬件有分支优化,也可显式进行分支优化likely,要优化条件分支,通常这些分支代码应该满足:该分支是热点代码的一部分,并且分支预测错误率较高,这样才能得到好的优化收益。 优化方法:

    ​ a、尽量避免把判断放到循环中;

    ​ b、拆分循环(循环中分支非常多会可能导致处理器分支预测失败率增加,把它拆分成几个小循环有可能改善处理器的分支预测正确率;在另外一些循环代码中,循环内分支条件依赖循环变量i,这种情况可通过拆分循环去掉分支,如常见的奇偶分支模式(循环内做一次奇数一次偶数减少判断/拆成两个循环i+=2))

    //奇偶分支模式,循环中需要判断奇偶,会出现一半分支预测失败
    for(int i = 0; i<size;  i++){
      if(i & 1) do_odd();
      elkse do_even();
    }
    // 循环展开进行优化
    for(int i = 0; i<size;  i++){
      do_odd();
      do_even();
    }
    // 如果寄存器使用太多考虑拆成两个循环
    for(int i = 0; i<size;  i+=2){
      do_odd();
    }
    for(int i = 0; i<size;  i+=2){
      do_even();
    }
    

    ​ c、合并多个条件:合并分支条件要求在分支执行前计算出分支的结果,采用临时变量存分支计算的结果,再进行if判断,则只需要一个分支预测。int x = (a0|a1|a2); if(0 == x){ }

    ​ d、使用条件状态值生成掩码来移除条件分支:即运用算法式来替换分支预测失败较多的判断

    ​ e、使用条件复制指令:编译器会将C语 言中简单的三目运算符编译成条件复制指令。

    ​ g、查表法移除分支:将各分支结果预计算到表中,将分支条件作为key查表法进行直接取值

    ​ h、分支顺序:如if(a&&b),将计算量大的放在后面,如果计算量差不多,把失败概率大的放前面。||同理。

  7. 指令级别:不同指令的吞吐量和延迟并不相同,应优先使用吞吐量大、延迟小的指令。

    (1)减少数据依赖:数据依赖会减弱处理器的乱序访问性能,另外读写依赖会极大地减弱内存性能

    // 只依赖寄存器
    float tmp = 0.0f;
    for(int i = 0; i < len; i++){
    	tmp += a[i];
    	a[i] = tmp; // 只依赖与tmp
    }
    // 替换掉下面数据依赖代码
    for(int i = 1; i < len; i++){
    	a[i] += a[i-1]; // 每次循环读a[i-1]依赖于上一次写入
    }
    

    (2)注意处理器的多发射能力:尽量将能够同时发射的指令安排在一起,不过这可能要求使用内置函数或汇编语言编写代码

    (3)优化乘除法和模余:一般整数的位运算最多只要一个周期,而乘法要三个周期,除法十几个,模余需要几 十甚至上百个,而通常移位运算只要一个周期。 可以将除法转化为乘法,或者保存某些除法的中间结果,必要时甚至可 以使用某些近似算法。 整数乘以一个整数可以转变成左移操作,如果乘数是常量,编译器会自动执行这种转换。整数除法和模余是非常耗时的,要少使用,如果是除以或模2的幂,则可转变为右移或位与运算。

    (4)选择更具体的库函数和算法

    (5)其他:如声明float时加f后缀,使用const、static,少用虚函数等。尽量给编译器更具体更多 的信息,以便编译器能够做出更好的优化决定

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值