http最高版本优化了哪些性能

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了http最高版本优化了哪些性能相关的知识,希望对你有一定的参考价值。

参考技术A Tomcat性能调优方案
一、操作系统调优
对于操作系统优化来说,是尽可能的增大可使用的内存容量、提高CPU的频率,保证文件系统的读写速率等。经过压力测试验证,在并发连接很多的情况下,CPU的处理能力越强,系统运行速度越快。。
【适用场景】 任何项目。
二、Java虚拟机调优
应该选择SUN的JVM,在满足项目需要的前提下,尽量选用版本较高的JVM,一般来说高版本产品在速度和效率上比低版本会有改进。
JDK1.4比JDK1.3性能提高了近10%-20%,JDK1.5比JDK1.4性能提高25%-75%。
因此对性能要求较高的情况推荐使用 JDK1.6。
【适用场景】 任何项目。
三、Apache集成Tomcat
Web服务器专门处理HTTP请求,应用服务器是通过很多协议为应用提供商业逻辑。虽然Tomcat也可以作web服务器,但其处理静态html的速度比不上Apache,且其作为web服务器的功能远不如Apache,因此把Apache和Tomcat集成起来,将html和Jsp的功能部分进行明确分工,让Tomcat只处理Jsp部分,其他的由Apache,IIS等web服务器去处理,由此大大提高Tomcat的运行效率。
如果一个项目中大量使用了静态页面、大量的图片等,并有有较大的访问量,推荐使用Apache集成Tomcat的方式来提高系统的整体性能。
Apache和Tomcat的整合有三种方式,分别是JK、http_proxy和ajp_proxy.其中JK方式是最常见的方式,JK本身有两个版本分别是1和2,目前1最新版本是1.2.8,而版本2早已经废弃了。http_proxy是利用Apache自带的mod_proxy模块使用代理技术来连接Tomcat。Ajp_proxy连接方式其实跟http_proxy方式一样,都是由mod_proxy所提供的功能。只需要把配置中的http://换成ajp://,同时连接的是Tomcat的AJP Connector所在的端口。
相对于JK的连接方式,后两种在配置上比较简单的,灵活性方面也一点都不逊色。但就稳定性而言不像JK这样久经考验,所以建议采用JK的连接方式。
Apache+JK+Tomcat配置:
使用到的两个配置文件分别是:httpd.conf和mod_jk.conf。其中httpd.conf是Apache服务器的配置文件,用来加载JK模块以及指定JK配置文件信息。mod_jk.conf是到Tomcat服务器的连接定义文件。
【部署步骤】
1.安装Apache服务器
2.部署Tomcat
3.将mod_jk.so拷贝到modules目录下面
4.修改httpd.conf和mod_jk.conf
【适用场景】 大量使用静态页面的应用系统。
四、Apache和Tomcat集群
对于并发要求很高的系统,我们需要采取负载均衡的方式来分担Tomcat服务器的压力。负载均衡实现大概有四种:第一是通过DNS,但只能简单的实现轮流分配,不能处理故障;第二是基于MS IIS,windows 2003 server本身就带了负载均衡服务;第三是硬件方式,通过交换机功能或专门的负载均衡设备来实现;第四种是软件的方式,通过一台负载均衡服务器进行,上面安装软件。使用Apache Httpd Server做负载均衡器,Tomcat集群节点使用Tomcat就可以做到上述第四种方式,这种方式比较灵活,成本相对比较低,另外一个很大的优点就是可以根据应用情况和服务器的情况做一些灵活的配置。所以推荐使用Apache+Tomcat集群来实现负载均衡。
采用Tomcat集群可以最大程度的发挥服务器的性能,可以在配置较高的服务器上部署多个Tomcat,也可以在多台服务器上分别部署Tomcat,Apache和Tomcat整合的方式还是JK方式。经过验证,系统对大用户量使用的响应方面,Apache+3Tomccat集群> Apache+2Tomcat集群 > Apache集成Tomcat > 单个Tomcat。并且采用Apache+多Tomcat集群的部署方式时,如果一个Tomcat出现宕机,系统可以继续使用,所以在硬件系统性能足够优越的情况下,需要尽量发挥软件的性能,可以采用增加Tomcat集群的方式。
Apache+Tomcat集群的方式使用到得配置文件有httpd.conf、mod_jk.conf、workers.properties。其中mod_jk.conf是对JK信息的配置,包括JK的路径等,workers.properties配置文件是对Tomcat服务器的连接定义文件。
Apache需要调整运行参数,这样才能构建一个适合相应网络环境的web服务。其中可进行的优化配置如下:
1. 设置MPM(Multi Processing Modules多道处理模块)。ThreadPerChild,这个参数用于设置每个进程的线程数,在Windows环境下默认值是64,最大值是1920,建议设置为100-500之间,服务器性能高的话值大一些,反之小一些。MaxRequestPerChild表示每个子进程能够处理的最大请求数。这个参数的值更大程度上取决于服务器的内存,如果内存比较大的话可以设置为很大的参数,否则设置一个较小的值,建议值是3000.
2. 关闭DNS和名字解析 HostnameLookups off
3. 打开UseCanonicalName模块 UseCanonicalName on
4. 关闭多余模块 一般来说,不需要加载的模块有,mod_include.so、mod_autoindex.so、mod_access.so、mod_auth.so.
5. 打开KeepAlive支持
KeepAlive on, KeepAliveTimeout 15 MaxKeepAliveRequests 1000
根据实际经验,通过Apache和Tomcat集群的方式提高系统性能的效果十分明显,这种方式可以最大化的利用硬件资源,通过多个Tomcat的处理来分担单Tomcat时的压力。
【部署步骤】
1.安装Apache服务器
2.部署Tomcat集群,即多个相同的Tomcat。
3.将mod_jk.so拷贝到modules目录下面
4.修改httpd.conf、mod_jk.conf和workers.properties
【适用场景】 并发用户量及在线使用用户数量比较高的系统。
五、Tomcat自身优化
1. JVM参数调优:-Xms<size> 表示JVM初始化堆的大小,-Xmx<size>表示JVM堆的最大值。这两个值的大小一般根据需要进行设置。当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃。因此一般建议堆的最大值设置为可用内存的最大值的80%。在catalina.bat中,设置JAVA_OPTS='-Xms256m -Xmx512m',表示初始化内存为256MB,可以使用的最大内存为512MB。
2. 禁用DNS查询
当web应用程序向要记录客户端的信息时,它也会记录客户端的IP地址或者通过域名服务器查找机器名转换为IP地址。DNS查询需要占用网络,并且包括可能从很多很远的服务器或者不起作用的服务器上去获取对应的IP的过程,这样会消耗一定的时间。为了消除DNS查询对性能的影响我们可以关闭DNS查询,方式是修改server.xml文件中的enableLookups参数值:
Tomcat4

<Connector className="org.apache.coyote.tomcat4.CoyoteConnector" port="80" minProcessors="5" maxProcessors="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" useURIValidationHack="false" disableUploadTimeout="true" />

Tomcat5

<Connector port="80" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" disableUploadTimeout="true"/>
3. 调整线程数
通过应用程序的连接器(Connector)进行性能控制的的参数是创建的处理请求的线程数。Tomcat使用线程池加速响应速度来处理请求。在Java中线程是程序运行时的路径,是在一个程序中与其它控制线程无关的、能够独立运行的代码段。它们共享相同的地址空间。多线程帮助程序员写出CPU最大利用率的高效程序,使空闲时间保持最低,从而接受更多的请求。
Tomcat4中可以通过修改minProcessors和maxProcessors的值来控制线程数。这些值在安装后就已经设定为默认值并且是足够使用的,但是随着站点的扩容而改大这些值。minProcessors服务器启动时创建的处理请求的线程数应该足够处理一个小量的负载。也就是说,如果一天内每秒仅发生5次单击事件,并且每个请求任务处理需要1秒钟,那么预先设置线程数为5就足够了。但在你的站点访问量较大时就需要设置更大的线程数,指定为参数maxProcessors的值。maxProcessors的值也是有上限的,应防止流量不可控制(或者恶意的服务攻击),从而导致超出了虚拟机使用内存的大小。如果要加大并发连接数,应同时加大这两个参数。web server允许的最大连接数还受制于操作系统的内核参数设置,通常Windows是2000个左右,Linux是1000个左右。
在Tomcat5对这些参数进行了调整,请看下面属性:
maxThreads Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数。
acceptCount 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。
connnectionTimeout 网络连接超时,单位:毫秒。设置为0表示永不超时,这样设置有隐患的。通常可设置为30000毫秒。
minSpareThreads Tomcat初始化时创建的线程数。

maxSpareThreads 一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。
最好的方式是多设置几次并且进行测试,观察响应时间和内存使用情况。在不同的机器、操作系统或虚拟机组合的情况下可能会不同,而且并不是所有人的web站点的流量都是一样的,因此没有一刀切的方案来确定线程数的值。
六、APR库使用
Tomcat中使用APR库,其实就是在Tomcat中使用JNI的方式来读取文件以及进行网络传输。可以大大提升Tomcat对静态文件的处理性能,同时如果你使用了HTTPS方式传输的话,也可以提升SSL的处理性能。
一般在Windows下,可以直接下载编译好的二进制版本的dll库文件来使Tomcat启用APR,一般建议拷贝库文件tcnative-1.dll到Tomcat的bin目录下。而在Linux下,可以直接解压和安装bin目录下的tomcat_native.tar.gz文件,编译之前要确保apr库已经安装。
怎么才能判断Tomcat是否已经启用了APR库呢?方法是通过看Tomcat的启动日志:
如果没有启用APR,则启动日志一般有这么一条:
org.apache.coyote.http11.Http11Protocol start
如果启用了APR,则这条日志就会变成:
org.apache.coyote.http11.Http11AprProtocol start
tcnative-1.dll 下载地址:http://tomcat.heanet.ie/native/
调优综述
根据以上分析,如果想要Tomcat达到最优的效果,首先要争取使得操作系统以及网络资源达到最优,并且最好使用高版本的JDK。对于有大量静态页面的系统,采用Apache集成Tomcat的方式,把静态页面交由Apache处理,动态部分交由Tomcat处理,能极大解放Tomcat的处理能力。使用ARP库也能极大的提高Tomcat对静态文件的处理能力。对于并发要求较高的系统,采用Apache加Tomcat集群的方式,将负载分别分担到多个Tomcat上,能很大的提高系统的性能,充分利用硬件资源。同时需要对Tomcat自身进行优化,包括增大内存、调节并发线程数等。本回答被提问者采纳

编译器都做了哪些优化?


这是优化系列文章,因为编译优化是成本收益比最高的优化手段,这篇文章主要介绍编译器是如何进行代码优化的,能做什么优化,不能做什么优化,如何充分利用好编译器的优化选项。


现代编译器可以对代码进行大量修改,以提高性能。对于程序员来说,知道编译器能做什么和不能做什么是很有用的,程序员需要了解这些优化。


函数内联


编译器可以用被调用函数的函数体替换函数调用,例如:

float square(float a) { return a * a;}float parabola(float x) { return square(x) + 1.0f;}


编译器可以用square内部的代码替换对square的调用:

float parabola(float x) { return x * x + 1.0f;}


内联的优势:

1

消除了调用、返回和参数传递的开销。 

2

代码缓存效率更高,因为代码变得连续。 

3

如果只有一个内联函数调用,代码就会变小。


函数内联的缺点是,如果对内联函数调用多次,而且函数很大,那么代码就会变大。


常量折叠和常量传播


只包含常量的表达式或子表达式将被计算结果替换。例如:

double a, b;a = b + 2.0 / 3.0;


编译器将会被替换成这样:

a = b + 0.666666666666666666667;


这其实很方便。编写2.0/3.0比计算值并编写许多小数更容易。建议在这样的子表达式周围加一个括号,以确保编译器能识别出它是子表达式。例如,除非我们在常量子表达式周围加了圆括号,否则b*2.0/3.0将被计算为(b*2.0)/3.0而不是b*(2.0/3.0)。


常量可以通过一系列计算传播:

float parabola(float x) { return x * x + 1.0f;}float a, b;a = parabola(2.0f);b = a + 1.0f;


编译器可能会替换成这样:

a = 5.0f;b = 6.0f;


如果表达式包含无法内联或无法在编译时计算的函数,则不可能进行常量折叠和常量传播。例如:

double a = sin(0.8);


sin函数是在一个单独的函数库中定义的,我们不能期望编译器能够内联这个函数,并在编译时计算它。有些编译器能够在编译时计算最常见的数学函数,如sqrt和pow,但不能计算更复杂的函数,如sin。


指针消除


如果指针指向的对象已知,则可以删除指针或引用。例如:

void plus(int *p) { *p = *p + 2;}int a;plus(&a);


编译器会替换成这样:

a += 2;


公共子表达式消除


如果同一个子表达式出现多次,那么编译器只能计算一次,例如:

int a, b, cb = (a+1) * (a+1);c = (a+1) / 4;


编译器会替换成这样:

int a, b, c, temp;temp = a + 1;b = temp * temp;c = temp / 4;


寄存器变量


最常用的变量存储在寄存器中。整数寄存器变量在32位系统中最多大约是6个,在64位系统中大约是14个。浮点寄存器变量在32位系统中最多大约是8个,在64位系统中是16个,在64位代码中启用AVX512指令集时是32个。除非启用了高版本指令集,否则一些编译器在32位系统中,很难创建浮点寄存器变量。


编译器会选择合适的变量存储在寄存器中,包括指针和引用,它们可以存储在整数寄存器中。寄存器变量的典型候选者是临时中间变量、循环计数器、函数参数、指针、引用、this指针、公共子表达等。


如果一个变量被取址,也就是说,如果有一个指向该变量的指针或引用,那么该变量就不能存储在寄存器中。因此,我们应该避免使用任何指向可以从寄存器存储中受益的变量的指针或引用。


活动范围分析


变量的活动范围是使用该变量的代码范围。如果它们的活动范围没有重叠,或者它们肯定有相同的值,编译器优化可以为多个变量使用相同的寄存器。当寄存器的数量有限时,这个优化很有用,例如:

int SomeFunction(int a, int x[]) { int b, c; x[0] = a; b = a + 1; x[1] = b; c = b + 1; return c;}


这里a、b和c可以共享相同的寄存器,因为它们的活动范围没有重叠。但如果c = b + 1改为c = a + 2,那么a和b不能使用相同的寄存器,因为它们的活动范围重叠了。


编译器通常不会对内存中的变量使用这个原则,即使它们的活动范围没有重叠,它也不会为不同的变量使用相同的内存区域。


为分支提取相同的代码片段


通过提取相同的代码片段,代码可以变得更加紧凑。

double x, y, z; bool b;if (b) { y = sin(x); z = y + 1;} else { y = cos(x); z = y + 1;}


编译器可以替换为:

double x, y; bool b;if (b) { y = sin(x);} else { y = cos(x);}z = y + 1;


消除跳转


编译器可以通过复制它跳转到的代码来避免跳转,例如:

int someFunction(int a, bool b) { if (b) a = a * 2; else a = a * 3; return a + 1;}

代码里有一个从a=a*2到return a+1的跳转,编译器可以通过复制return语句来消除跳转:

int someFunction(int a, bool b) { if (b) { a = a * 2; return a + 1; } else { a = a * 3; return a + 1; }}

如果条件可以简化为经常为true或者经常为false时,那分支可以被消除,例如:

if (true) { a = b;} else { a = c;}


这个可以被消除为:

a = b;


如果从前一个分支中知道条件,也可以消除分支。

int someFunction(int a, bool b) { if (b) a = a * 2; else a = a * 3; if (b) return a + 1; else return a - 1;}


编译器可以简化为:

int someFunction(int a, bool b) { if (b) { a = a * 2; return a + 1; } else { a = a * 3; return a - 1; }}


循环展开


编译器在高度优化下,会展开循环,它可以展开重复次数很低的循环,来避免循环的开销,例如:

int i, a[2];for (i = 0; i < 2; i++) a[i] = i+1;


编译器可以优化为:

int a[2];a[0] = 1;a[1] = 2;


然而有些编译器可能展开得太多,过多的循环展开不一定是好事,因为它占用了代码缓存中太多的空间,并且它填满了许多CPU的循环缓冲区。某些情况下,关闭编译器的loop unroll选项可能很有用。


循环中不变的运算代码


如果某些运算与循环计数器无关,可以将其移出循环。例如:

int i, a[100], b;for (i = 0; i < 100; i++) { a[i] = b * b + 1;}


编译器可以改变它为:

int i, a[100], b, temp;temp = b * b + 1;for (i = 0; i < 100; i++) { a[i] = temp;}


归纳推导变量


循环计数器的线性函数表达式可以通过在前一个值上加一个常数来计算。例如:

int i, a[100];for (i = 0; i < 100; i++) { a[i] = i * 9 + 3;


编译器可以改成这样来避免乘法操作:

int i, a[100], temp;temp = 3;for (i = 0; i < 100; i++) { a[i] = temp; temp += 9;}


struct S1 {double a; double b;};S1 list[100]; int i;for (i = 0; i < 100; i++) { list[i].a = 1.0; list[i].b = 2.0;}


struct S1 {double a; double b;};S1 list[100], *temp;for (temp = &list[0]; temp < &list[100]; temp++) { temp->a = 1.0; temp->b = 2.0;}


时序安排


为了并行执行,编译器可以对指令重新排序。例如:

float a, b, c, d, e, f, x, y;x = a + b + c;y = d + e + f;


在这个例子中,编译器可以交叉计算两个公式,首先计算a + b,然后就是d + e,然后c添加到第一个表达式,那么f被添加到第二个表达式,第一个结果是存储在x中,第二个结果存储在y中。这样可以帮助CPU做并行计算,CPU实际上可以在没有编译器的帮助下对指令重排序,但是编译器可以让CPU更容易地对指令进行重排序。


代数简化


编译器可以灵活运用代数运算定律来简化表达式,例如:

1

-(-a)简化为a

2

if(!a && !b)简化为if(!(a||b)) 

3

(a*b*c) + (a*b*c)简化为a * b * c * 2


整数运算时:

int a, b, c, y;y = a + b + c;


根据代数运算定律,我们可以写成:

y = c + b + a;


如果子表达式c+b可以在其他地方重用,这可能会很有用。


这种优化对于浮点数运算有些风险,因为浮点数交换顺序会影响精度,看这个例子:

float a = -1.0E8, b = 1.0E8, c = 1.23456, y;y = a + b + c;


这里的计算给出a+b=0,然后0+1.23456 = 1.23456。但是如果我们改变操作数的顺序,先加上b和c,就不会得到相同的结果。b + c = 100000001.23456。float类型的精度约为7位有效数字,因此b+c的值将四舍五入到100000000。当我们把a加上后,结果是0而不是1.23456。


虚函数优化


编译器如果知道基类指针或引用究竟指向哪个子类对象,那编译器就可以优化,可以绕过虚函数调用的虚函数表查找,如:

class C0 { public: virtual void f();};class C1 : public C0 { public: virtual void f();};void g() { C1 obj1; C0 *p = &obj1; p->f(); // Virtual call to C1::f}


如果没有优化,编译器需要在一个虚表中查找,用于确定p->f()是指向C0::f还是C1::f。如果编译器可以看到p总是指向类C1的对象,那它可以直接调用C1::f,而不需要使用虚表。然而貌似没有编译器能够进行这种优化。


编译器优化的障碍

编译器不是万能的,有些情况下它也不能优化:


不能够跨模块优化


除了正在编译的模块,编译器没有其他模块中函数的信息,所以它不能跨模块进行优化。例:

module1.cppint Func1(int x) { return x*x + 1;}module2.cppint Func2() { int a = Func1(2); ...}


如果Func1和Func2在同一个模块中,那么编译器将能够进行函数内联和常量传播,并将a优化为常量5。但是编译器在编译module2.cpp时没有关于Func1的必要信息。该问题最有效的解决办法,是通过#include指令将多个.cpp模块组合成一个。


指针别名


当通过指针或引用访问变量时,编译器不能完全排除被指向的变量,与其他变量相同的可能性。例如:

void Func1 (int a[], int * p) { int i; for (i = 0; i < 100; i++) { a[i] = *p + 2; }}void Func2() { int list[100]; Func1(list, &list[8]);}


这里,需要重新加载*p并计算*p+2表达式100次,因为p指向的值与a[]中的一个元素相同,在循环过程中会发生变化,所以编译器无法做优化。例子确实很坑,但关键是编译器不能排除这种例子存在的理论可能性。因此,编译器不能假定*p+2是一个循环不变表达式,它也就不能移到循环之外。


大多数编译器都有一个选项,来假设没有指针别名。我们需要仔细分析代码中的所有指针和引用,确保在代码的同一部分中,没有以多种方式访问变量或对象。如果编译器支持,也可以使用关键字__restrict或__restrict__来告诉编译器不会有别名。


我们永远不能确定编译器是否收到没有指针别名的提示,所以,确保代码被优化的最好办法是显式地执行。在上例中,如果我们确定指针没有为数组中的任何元素起别名,可以计算*p+2并将其存储在循环外的临时变量中,这种方法需要我们自己能够预测优化的障碍在哪里。


纯函数


纯函数是指没有副作用的函数,其返回值仅取决于其传入参数的值。相同参数的纯函数,每次调用都会产生相同的返回值。编译器可以优化纯函数,可以消除包含纯函数调用的公共子表达式,也可以移出包含纯函数调用的循环不变代码。但是如果是自定义的函数,编译器通常不知道该函数是否为纯函数。


因此,当涉及到纯函数调用时,有必要手动进行诸如公共子表达式消除、常量传播和循环不变代码移动等优化。


我们也可以告诉编译器函数是个纯函数。如:

#ifdef __GNUC__#define pure_function __attribute__((const))#else#define pure_function#endifdouble Func1(double) pure_function ;double Func2(double x) { return Func1(x) * Func1(x) + 1.;}


在这里,Gnu编译器将只调用Func1一次,而其他编译器将调用两次。


虚函数和函数指针


编译器几乎不可能准确预测,将调用哪个版本的虚函数,或者函数指针指向哪个函数。因此,编译器不能对虚函数和函数指针进行内联等优化。


代数简化


编译器可以对一些简单的整数运算进行代数简化优化,但是复杂的就无能为力了,而且也不能对浮点变量进行代数简化,编译器可以对哪种表达式进行优化,哪种不能进行优化,可以看表。


内联函数的非内联拷贝


函数内联有些复杂,因为同一函数可能会被另一个模块调用,编译器为了满足此隐藏条件,就需要对内联函数做一份拷贝,然而,如果没有其它模块调用此函数,那就浪费了这次拷贝操作,而且浪费代码空间。


怎么解决此问题呢?函数有没有被其它模块调用,编译器不知道,但是程序员是知道的,如果没有被其它模块调用,我们可以用static修饰函数,表示本地使用的意思,但是static修饰类成员函数却有不同意义,然而类成员函数可以直接使用inline修饰。


Linux编译器有个选项-function-sections,可以使链接器删除未引用的函数,我们可以打开这个选项。


编译器优化选项

所有的编译器都有各种各样的优化选项,我们可以手动选择是否打开,有必要研究我们正在使用的编译器优化选项。


编译器所有的优化选项可以看我之前的这篇文章:


一般我们开发的程序都有两个版本,Debug版本和Release版本,Debug版本是调试版本,该版本通常不带任何优化动作,发布程序的时候会使用Release版本,该版本会对程序做很多优化。优化有两个方向,一个是程序体积方向,一个是速度方向,当速度优化到极致时,有可能程序体积不是最小,当程序体积优化到最小时,有可能程序执行速度不是最快,我们应该自己权衡,通常情况下,最高的优化选项是最好的,只有某些情况下,它才不是最优解。


1

当不需要与老版本CPU兼容时,我们可以选择使用最新的指令集。

2

我们可以考虑不使用异常处理,并关闭编译器对异常处理的支持。

3

可以考虑关闭RTTI。

4

可以启用打开浮点数快速计算的选项。

5

如果我们确定代码没有指针别名,可以使用"assume no pointer aliasing"选项。

6

很多编译器使用标准栈帧选项,该栈帧用于调试和异常处理,会占用寄存器资源,而寄存器是一种稀缺资源,我们可以考虑选择省略标准栈帧选项,将寄存器用于其它目的。



优化指令

一些编译器有许多关键字和指令,用于在代码的特定位置给出特定的优化提示。某些指令中有许多是编译器特有的。不能指望Windows编译器的指令能在Linux编译器上工作,反之亦然。但是大多数Microsoft指令在Windows的Intel编译器和Windows的Gnu编译器上工作,而大多数Gnu指令在Linux的PathScale和Intel编译器上工作。


关键字volatile,确保变量永远不会存储在寄存器中,每次读写都是在内存中。它用于多个线程之间共享的变量,并关闭变量相关的很多优化。


const关键字表示变量永远不会改变,编译器在许多情况下可能优化掉此变量。如:

const int ArraySize = 1000;int List[ArraySize];...for (int i = 0; i < ArraySize; i++) List[i]++;



const指针或const引用不能改变它所指向的对象,const成员函数不能修改数据成员,我们可以考虑适当使用const关键字,给编译器更多优化提示,提高优化的可能性。


noexcept可修饰某些函数,用于提示编译器此函数永远不会产生异常,编译器可以对此进行更多优化。


static关键字根据上下文有几种含义。


当static修饰非成员函数时,表示该函数不能被其它任何模块访问,方便编译器进行内联和过程间优化。

当static修饰全局变量时,表示它不会被任何其他模块访问,方便编译器过程间优化。

当static修饰函数体内的局部变量时,表示该变量在函数返回时将被保留,并在下次调用函数时保持不变。这可能导致效率低下,因为一些编译器会插入额外的代码,以保护变量不被多个线程同时访问。即使变量是const,也是如此。


然而,有时候需要将局部变量设为static和const,确保只有在第一次调用函数时才对其进行初始化。如:

void Func () { static const double log2 = log(2.0); ...}


这里,log(2.0)只在第一次执行Func时计算,但如果没有static,每次调用Func时都会重新计算对数。这样做的缺点是,函数必须检查它以前是否被调用过,但这也比重新计算对数要快,将log2作为全局const变量,或用计算后的值替换它,速度会更快。


用static修饰类成员函数时,就代表它不能访问任何非静态数据成员或成员函数,静态成员函数比非静态成员函数调用得更快,因为它不需要this指针。我们可以适当的时候将成员函数用static修饰。


编译器特定的优化指令


1

快速函数调用:__attribute__ ((fastcall)),仅适用于32位模式,该饰符可以使函数调用在32位模式下更快,前两个整数参数在寄存器中传输,而不是在栈中传输。

2

纯函数:__attribute__((const)),Linux特有。

3

指定没有指针重叠:__declspec(noalias)或__restrict或#pragma optimize ("a",on),这些指令不一定经常有效。

4

数据对齐:__declspec(align(16))或__attribute__((aligned(16))),C++11中可用alignas(16)。



不同的编译器对比

不同编译器对于不同操作优化的支持能力数据来源于网络,这里我整理了一份脑图分享给大家,部分节点图如下:


参考资料

https://www.agner.org/optimize/


- EOF -

推荐阅读   点击标题可跳转

1、

2、

3、


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子