精华内容
下载资源
问答
  • 尽可能使用使编译器更容易产生高效代码的方式来进行程序编写 程序优化的第一步应当是消除多余的不必要的工作,包括函数调用、条件测试与内存引用等。这些优化不依赖于目标机器。 基于Intel和AMD处理器,本章提出了...

    优化程序性能

    本章主要探讨内容:使用几种不同的程序优化技术,让程序运行得更加高效,并且在可维护性与性能间达到平衡。

    将从以下的几个方面对程序进行优化

    1. 选择合适的算法与数据结构
    2. 尽可能使用使编译器更容易产生高效代码的方式来进行程序编写

    程序优化的第一步应当是消除多余的不必要的工作,包括函数调用、条件测试与内存引用等。这些优化不依赖于目标机器。
    基于Intel和AMD处理器,本章提出了一种高级模型。还设计了一种图形数据流来对处理器的指令执行形象化,并利用其预测程序的性能。

    利用处理器提供的指令级别并行能力同时执行多条指令,提高程序性能。
    通过研究程序的汇编代码是理解编译器以及产生的代码如何运行的最有效的手段之一。

    不管有什么样的策略,在落实的时候都需要不断地试错、不断地修改源码并分析其性能。

    优化编译器的能力和局限

    GCC向用户提供了一些对它们所使用的优化的控制。最简单的控制就是指定优化级别。具体内容在GCC手册中有详细的说明。
    一般使用-O2级别的优化。

    注意,编译器只对程序使用安全的优化
    所谓安全的优化,是指优化的结果不会导致任何运行结果的不同或者内容上的歧义。

    避免内存别名的出现以及使用restrict进行优化

    一个典型的例子——内存别名使用

    // code 1
    *a += *b;
    *a += *b;
    
    // code 2
    *a += 2 * *b;
    

    上述代码中,code2更加高效。
    code1需要6次内存读取,而code2只需要3次
    但是,编译器不会把code1优化为code2的形式——若指针a、指针b指向同一片内存,则code1的结果将与code2产生差异。

    这种两个指针可能指向同一个内存位置的情况称之为内存别名使用。编译器必须假设不同的指针可能会指向内存中的同一个位置。

    例如以下代码:

    x = 1000;
    y = 3000;
    *q = y;
    *p = x;
    t = *q;
    

    编译器必须假设指针p、q指向了同一片内存。若p、q内存地址不同,则t的取值为1000,否则为3000。
    这就是一个妨碍优化的因素。如果编译器不能确定两个指针是否指向同一个位置,就必须假设什么情况都有可能。

    另一个内存对齐的例子是(参考资料

    #include <stdio.h>
    
    void fun_1(int *target, int len, int *source);
    void fun_2(int *target, int len, int *source);
    
    int main()
    {
    	int arr[] = {1, 2, 3};
    	int *target = arr + 1;
    	fun_1(target, 3, arr);
    	// fun_2(target, 3, arr);
    	printf("%d\n", *target);
    	return 0;
    }
    
    void fun_1(int *target, int len, int *source)
    {
    	for (int i = 0; i < len; i ++)
    		*target += source[i];
    }
    
    void fun_2(int *target, int len, int *source)
    {
    	int sum = *target;
    	for (int i = 0; i < len; i ++)
    		sum += source[i];
    	*target = sum;
    }
    

    执行两个函数,得到的结果分别为9、8。

    在这种情况下,编译器绝对不会进行优化。

    在C99中添加了一个restrict指针,表示指针指向内存之间不存在相互覆盖
    在开启-O1优化的情况下,fun_1实现了与fun_2相同的效果。
    (另外:-O2-O3均无法获得相同的效果,因为在-O2级别的优化中,main函数根本未调用fun_2函数,结果在编译过程中已经计算完毕

    C++中未定义类似的关键字。
    但是可以使用__restrict__来实现一样的效果。

    • restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改
      使用方法:int * restrict ptr = (int*)malloc(sizeof(int) * 10);,表明ptr是该内存唯一的访问方式
    • memove与memcpy:
      void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
      void * memove(void * s1, const void * s2, size_t n);
      二者唯一区别是,memcpy假定两个内存不重叠,而memove不假定。
      判定内存是否重叠是程序员的工作。
    • 参考自:https://blog.csdn.net/llf021421/article/details/8092602

    restrict 关键字的一个应用:

    int f(int *x, int *y)
    {
    	*x = 0;
    	*y = 1;
    	return *x;
    }
    
    int g(int *restrict x, int *restrict y)
    {
    	*x = 0;
    	*y = 1;
    	return *x;
    }
    

    生成的汇编代码如下:

    1. -O1优化
    	.globl	f
    	.type	f, @function
    f:
    .LFB0:
    	.cfi_startproc
    	movl	$0, (%rdi)
    	movl	$1, (%rsi)
    	movl	(%rdi), %eax
    	ret
    	.cfi_endproc
    .LFE0:
    	.size	f, .-f
    	.globl	g
    	.type	g, @function
    g:
    .LFB1:
    	.cfi_startproc
    	movl	$0, (%rdi)
    	movl	$1, (%rsi)
    	movl	$0, %eax
    	ret
    	.cfi_endproc
    .LFE1:
    	.size	g, .-g
    

    生成的-O2代码

    	.globl	f
    	.type	f, @function
    f:
    .LFB0:
    	.cfi_startproc
    	movl	$0, (%rdi)
    	movl	$1, (%rsi)
    	movl	(%rdi), %eax
    	ret
    	.cfi_endproc
    .LFE0:
    	.size	f, .-f
    	.p2align 4,,15
    	.globl	g
    	.type	g, @function
    g:
    .LFB1:
    	.cfi_startproc
    	movl	$0, (%rdi)
    	movl	$1, (%rsi)
    	xorl	%eax, %eax
    	ret
    	.cfi_endproc
    .LFE1:
    	.size	g, .-g
    

    restrict关键字可以轻松地榨干编译器的最后一点性能,但是程序员必须遵守契约
    你必须保证,在这个指针的生命周期内,它独享它指向的那一块内存区域

    所以,提升性能的方法之一,是养成良好的代码规范,尽量不写有歧义的代码,并充分利用语言的特点。而编译器通常情况下也会很配合。


    避免函数调用对编译器优化的影响

    当某个函数可能会影响到全局程序状态的时候,例如改变全局变量等的时候,它会成为妨碍编译器优化的因素。
    大多数编译器不会试图判断一个函数是不是没有副作用。它会假设最糟糕的情况,并保持所有的函数调用不变

    使用内联函数优化函数调用
    包含函数调用的代码可以使用内联函数替换进行优化。
    这种替换减少了函数调用的开销,也允许对展开的代码进行进一步的优化

    GCC可以使用命令行-finline进行这种形式的优化,也能在使用优化等级为-O1以上时进行优化。

    但是GCC只会尝试在单个文件中定义的函数的内联。

    在某些情况下,最好不使用內联优化

    1. 使用符号调试器测试代码时,内联优化的函数调用无法被测试
    2. 使用代码剖析的方式评估代码性能,也将失败。
    • 使用inline修饰符的函数才能进行內联优化(在使用-finline的时候)
      关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用——inline 是一种 “用于实现的关键字” ,而不是一种“用于声明的关键字”
      内联函数只时用于简单代码的函数使用,不能包含复杂的结构控制语句,且不允许是递归函数——编译器会忽视。
      内联的代价是源码量变多——函数体内代码较长、出现循环等,都将导致执行函数体内代码的时间更长(但是编译器能进行进一步的优化,是否应当出现循环是不绝对的,需要具体问题具体分析)

    使用-finlineinline关键字,会将该函数在符号表中抹去
    高优化等级会尽可能对所有简单函数尝试进行内联优化
    但是高优化等级虽然也进行了内联优化,汇编代码中仍然有该函数的存在。

    总结

    在编译器的角度上,我们需要编写尽可能没有歧义的代码,以便编译器能进行优化。可以使用restrict关键字告诉编译器,一个函数中的两个指针不会指向同一片内存区域。但是前提是程序员要遵守约定。
    在写函数的时候,要注意,由于编译器不会判断函数是否会改变程序状态,因此,编译器不会对函数调用进行优化
    如果我们需要将一段简单的代码编写为函数,可以考虑使用内联优化以获得更高的效率。但是內联优化在某些时候不适用,使用内联优化关键字inline的函数,在汇编的时候会完全被抹除。

    展开全文
  • MLton 是整个程序的优化编译器的标准ML编程语言 MLton 是标准 ML 编程语言的全程序优化编译器。 Mlton 具有以下特点。 + 便携性。 在以下平台上运行。 o ARM:Linux (Debian)。 o Alpha:Linux (Debian)。 o ...
  • java的即时编译器特点

    2016-09-14 09:11:27
    具有很大的时间压力,它能提供的优化手段也严重受制于编译成本,如果编译速度不能达到要求,那用户将在启动程序或程序的某部分察觉到重大延迟,这点使得即时编译器不敢随便引入大规模的优化技术,而编译的时间成本在...
    java的即时编译器
    
    1)因为即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,它能提供的优化手段也严重受制于编译成本,如果编译速度不能达到要求,那用户将在启动程序或程序的某部分察觉到重大延迟,这点使得即时编译器不敢随便引入大规模的优化技术,而编译的时间成本在静态编译器中并不需要关注。
    2)Java语言是动态的类型安全语言,这就意味着需要由虚拟机来确保程序不会违反语言语义或访问非结构化的内存。从实现层面上看,这就意味着虚拟机必须频繁地进行动态检查,如实例方法访问时检查空指针、数组元素访问时检查上下界范围等。对于这类程序代码没有明确地写出检查行为,尽管编译器会努力优化,但是总体上仍然消耗不少时间
    3)java语言虽然没有virtual关键字,但是使用虚方法的频率却远远大于C++/C语言,这意味着运行时对方法接收者进行多台选择的频率远远大于C/C++,也意味着即时编译器在进行一些优化时的难度大于C/C++。
    4)java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,这使得很多全局优化都难以进行,因为编译器无法看见程序的全貌,许多全局的优化措施都只能以基激进的方式来完成,编译器不得不时刻注意并随着类型的变化而在运行时撤销或重新进行一些优化。

    5)java语言中对象的内存分配是在堆上分配的,只有方法中的局部变量才在栈上分配,而C++/C的对象有多种内存分配方式,既可以在堆上也可以在栈上分配,如果可以在栈上分配线程私有对象,将减轻内存回收的压力。

    展开全文
  • 浅谈编译器优化

    2013-08-06 13:22:43
    关于优化,可以说途径千千万万,没有一定的法则,但大抵有那么几个比较基本的法则,比如:充分了解计算机的体系结构,了解自己所处理的数据的特点,熟悉编译器的工作原理等等。今天我想谈谈关于编译器方面的一些见解...
    关于优化,可以说途径千千万万,没有一定的法则,但大抵有那么几个比较基本的法则,比如:充分了解计算机的体系结构,了解自己所处理的数据的特点,熟悉编译器的工作原理等等。今天我想谈谈关于编译器方面的一些见解,编译器分为预处理,编译,链接等大概的几个部分,如果能够清楚这几个阶段编译器各做了什么事情,编译器在哪方面比较聪明,能够替你做一些优化,又在哪方面比较弱智,无法替你做优化,你就能很清醒地知道,你做的哪些优化是有效的,下面举例说明:
    


    今天有人提出在汇编指令中,去掉一些地址偏移乘法计算,这种善于动脑筋的方法值得赞赏,但可能对编译过程理解不是很透彻,会造成认识上的一些偏差。

    比如如下指令:
    movq 8*4(%%rsi),%%rax
    这是一个移动数据的指令,操作的对象是一个128位的数,其中源操作数的选址方式是寄存器间接寻址,地址偏移量是8*4,目的操作数是一个128位的普通寄存器。很显然,源操作数有一个地址偏移计算的过程,其过程常规理解应该是,首先计算8*4,得到32,然后将32加到地址寄存器%%rsi中去,然后再去执行寻址内存的操作,那么按照这个理解,我们想当然可以提出如下优化:
    movq 32(%%rsi),%%rax
    很显然这个过程少了一个计算8*4=32的过程,应该能起到优化作用,但果真如此吗??答案是否定的,这种替换没有任何优化作用,反倒代码可读性可能降低,这是为什么呢?原因是我们混淆了操作符的执行时期,我们一直在说,new/delete 是操作符,sizeof是操作符+ - * / %是操作符,但我们是否真正理解了什么是操作符吗,那些操作符操作是在编译期执行的,根本没有转化成指令,那些操作符操作最终是要转化为执行期的指令呢,我们不妨举个例子:
    假设有如下C语言函数
    int main()
    {
        int a;
        int b;
        a = 3 * 5 - 2 + 1;
        b = a + 9 * 6 + 1;
        return 0;
    }

    执行

    gcc -S compile.c 

    得到如下compile.s汇编代码:

    movl $14,-8(%%ebp)
    movl -8(%%ebp),%%eax
    addl $55,%%eax
    movl %%eax,-4(%%ebp)

    其中

    -8(%%ebp),-4(%%ebp)

    分别代表a,b变量在内存中的地址,很显然,经过一次编译已经完成了如下操作:

    3 * 5 - 2 + 1 = 14
    9 * 6 + 1 = 55
    换句话说这些操作根本没有转化为执行期的代码,当然也就无所谓效率提高不提高了
    其实主函数内赋值操作的代码完全等同于:
    a = 14;
    b = a + 55;

    那到底那些代码能够最终转化为执行期间的代码呢,一个原则,就是操作符其中的一个操作对象是内存中的值,比如,a = 14 是一个赋内存变量值操作,最终转化为了

    movl $14,-8(%%ebp)

    指令.

    而在b = a + 55 中,加法操作转化为
    movl -8(%%ebp),%%eax
    addl $55,%%eax
    即:首先去取内存变量a的值,然后完成加法操作
    而赋值操作转化为:
    movl %%eax,-4(%%ebp)
    将最终的和值送给内存变量b所在的地址。

    由上可知,懂得编译器的工作原理,对优化工作来说还是比较重要的



    展开全文
  • 作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见“JVM性能优化,Part 1″中对JVM的介绍)。Eva Andreasson将对不同种类的编译器做介绍,并比较...

    转自: http://www.importnew.com/2009.html

    作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见“JVM性能优化,Part 1″中对JVM的介绍)。Eva Andreasson将对不同种类的编译器做介绍,并比较客户端、服务器端和层次编译产生的编译结果在性能上的区别,此外将对通用的JVM优化做介绍,包括死代码剔除、内联以及循环优化。

    Java编译器存在是Java编程语言能独立于平台的根本原因。软件开发者可以尽全力编写程序,然后由Java编译器将源代码编译为针对于特定平台的高效、可运行的代码。不同类型的编译器适合于不同应用程序的需求,使编译结果可以满足期望的性能要求。对编译器基本原理了解得越多,在优化Java应用程序性能时就越能得心应手。

    什么是编译器

    简单来说,编译器就是将一种编程语言作为输入,输出另一种可执行语言的工具。大家都熟悉的javac就是一个编译器,所有标准版的JDK中都带有这个工具。javac以Java源代码作为输入,将其翻译为可由JVM执行的字节码。翻译后的字节码存储在.class文件中,在启动Java进程的时候,被载入到Java运行时中。

    标准CPU并不能识别字节码,它需要被转换为当前平台所能理解的本地指令。在JVM中,有专门的组件负责将字节码编译为平台相关指令,实际上,这也是一种编译器。有些JVM编译器可以处理多层级的编译工作,例如,编译器在最终将字节码转换为平台相关指令前,会为相关的字节码建立多层级的中间表示(intermediate representation)。

    字节码与JVM

    如果你想了解更多有关字节码与JVM的信息,请阅读 “Bytecode basics”(Bill Venners, JavaWorld)

    以平台未知的角度看,我们希望尽可能的保持平台独立性,因此,最后一级的编译,也就是从最低级表示到实际机器码的转换,是与具体平台的处理器架构息息相关的。在最高级的表示上,会因使用静态编译器还是动态编译器而有所区别。在这里,我们可以选择应用程序所以来的可执行环境,期望达到的性能要求,以及我们所面临的资源限制。在本系列的第1篇文章的静态编译器与动态编译器一节中,已经对此有过简要介绍。我将在本文的后续章节中详细介绍这部分内容。

    静态编译器与动态编译器

    前文提到的javac就是使用静态编译器的例子。静态编译器解释输入的源代码,并输出程序运行时所需的可执行文件。如果你修改了源代码,那么就需要使用编译器来重新编译代码,否则输出的可执行性文件不会发生变化;这是因为静态编译器的输入是静态的普通文件。

    使用静态编译器时,下面的Java代码

    1
    2
    3
    static int add7( int x ) {
          return x+ 7 ;
    }

    会生成类似如下的字节码:

    1
    2
    3
    4
    iload0
    bipush 7
    iadd
    ireturn

    动态编译器会动态的将一种编程语言编译为另一种,即在程序运行时执行编译工作。动态编译与优化使运行时可以根据当前应用程序的负载情况而做出相应的调整。动态编译器非常适合用于Java运行时中,因为Java运行时通常运行在无法预测而又会随着运行而有所变动的环境中。大部分JVM都会使用诸如Just-In-Time编译器的动态编译器。这里面需要注意的是,大部分动态编译器和代码优化有时需要使用额外的数据结构、线程和CPU资源。要做的优化或字节码上下文分析越高级,编译过程所消耗的资源就越多。在大多数运行环境中,相比于经过动态编译和代码优化所获得的性能提升,这些损耗微不足道。

     JVM的多样性与Java平台的独立性

    所有的JVM实现都有一个共同点,即它们都试图将应用程序的字节码转换为本地机器指令。一些JVM在载入应用程序后会解释执行应用程序,同时使用性能计数器来查找“热点”代码。还有一些JVM会调用解释执行的阶段,直接编译运行。资源密集型编译任务对应用程序来说可能会产生较大影响,尤其是那些客户端模式下运行的应用程序,但是资源密集型编译任务可以执行一些比较高级的优化任务。更多相关内容请参见相关资源

    如果你是Java初学者,JVM本身错综复杂结构会让你晕头转向的。不过,好消息是你无需精通JVM。JVM自己会做好代码编译和优化的工作,所以你无需关心如何针对目标平台架构来编写应用程序才能编译、优化,从而生成更好的本地机器指令。

    从字节码到可运行的程序

    当你编写完Java源代码并将之编译为字节码后,下一步就是将字节码指令编译为本地机器指令。这一步会由解释器或编译器完成。

    解释

    解释是最简单的字节码编译形式。解释器查找每条字节码指令对应的硬件指令,再由CPU执行相应的硬件指令。

    你可以将解释器想象为一个字典:每个单词(字节码指令)都有准确的解释(本地机器指令)。由于解释器每次读取一个字节码指令并立即执行,因此它就没有机会对某个指令集合进行优化。由于每次执行字节码时,解释器都需要做相应的解释工作,因此程序运行起来就很慢。解释执行可以准确执行字节码,但是未经优化而输出的指令集难以发挥目标平台处理器的最佳性能。

    编译

    另一方面,编译执行应用程序时,*编译器*会将加载运行时会用到的全部代码。因为编译器可以将字节码编译为本地代码,因此它可以获取到完整或部分运行时上下文信息,并依据收集到的信息决定到底应该如何编译字节码。编译器是根据诸如指令的不同执行分支和运行时上下文数据等代码信息来指定决策的。

    当字节码序列被编译为机器代码指令集合时,就可以对这个指令集合做一些优化操作了,优化后的指令集合会被存储到成为code cache的数据结构中。当下一次执行这部分字节码序列时,就会执行这些经过优化后被存储到code cache的指令集合。在某些情况下,性能计数器会失效,并覆盖掉先前所做的优化,这时,编译器会执行一次新的优化过程。使用code cache的好处是优化后的指令集可以立即执行 —— 无需像解释器一样再经过查找的过程或编译过程!这可以加速程序运行,尤其是像Java应用程序这种同一个方法会被多次调用应用程序。

    优化

    随着动态编译器一起出现的是性能计数器。例如,编译器会插入性能计数器,以统计每个字节码块(对应与某个被调用的方法)的调用次数。在进行相关优化时,编译器会使用收集到的数据来判断某个字节码块有多“热”,这样可以最大程度的降低对当前应用程序的影响。运行时数据监控有助于编译器完成多种代码优化工作,进一步提升代码执行性能。随着收集到的运行时数据越来越多,编译器就可以完成一些额外的、更加复杂的代码优化工作,例如编译出更高质量的目标代码,使用运行效率更高的代码替换原代码,甚至是剔除冗余操作等。

    示例

    考虑如下代码:

    1
    2
    3
    static int add7( int x ) {
          return x+ 7 ;
    }

    这段代码经过javac编译后会产生如下的字节码:

    1
    2
    3
    4
    iload0
    bipush 7
    iadd
    ireturn

    当调用这段代码时,字节码块会被动态的编译为本地机器指令。当性能计数器(如果这段代码应用了性能计数器的话)发现这段代码的运行次数超过了某个阈值后,动态编译器会对这段代码进行优化编译。后带的代码可能会是下面这个样子:

    1
    2
    lea rax,[rdx+ 7 ]
    ret

    各擅胜场

    不同的Java应用程序需要满足不同的需求。相对来说,企业级服务器端应用程序需要长时间运行,因此可以做更多的优化,而稍小点的客户端应用程序可能要求快速启动运行,占资源少。接下来我们考察三种编译器设置及其各自的优缺点。

    客户端编译器

    即大家熟知的优化编译器C1。在启动应用程序时,添加JVM启动参数“-client”可以启用C1编译器。正如启动参数所表示的,C1是一个客户端编译器,它专为客户端应用程序而设计,资源消耗更少,并且在大多数情况下,对应用程序的启动时间很敏感。C1编译器使用性能计数器来收集代码的运行时信息,执行一些简单、无侵入的代码优化任务。

    服务器端编译器

    对于那些需要长时间运行的应用程序,例如服务器端的企业级Java应用程序来说,客户端编译器所实现的功能还略有不足,因此服务器端的编译会使用类似C2这类的编译器。启动应用程序时添加命令行参数“-server”可以启用C2编译器。由于大多数服务器端应用程序都会长时间运行,因此相对于运行时间稍短的轻量级客户端应用程序,在服务器端应用程序中启用C2编译器可以收集到更多的运行时数据,也就可以执行一些更高级的编译技术与算法。

    提示:给服务器端编译器热身

    对于服务器端编译器来说,在应用程序开始运行之后,编译器可能会在一段时间之后才开始优化“热点”代码,所以服务器端编译器通常需要经过一个“热身”阶段。在服务器端编译器执行性能优化任务之前,要确保应用程序的各项准备工作都已就绪。给予编译器足够多的时间来完成编译、优化的工作才能取得更好的效果。(更多关于编译器热身与监控原理的内容请参见JavaWorld的文章”Watch your HotSpot compiler go“。)

    在执行编译任务优化任务时,服务器端编译器要比客户端编译器综合考虑更多的运行时信息,执行更复杂的分支分析,即对哪种优化路径能取得更好的效果作出判断。获取的运行时数据越多,编译优化所产生的效果越好。当然,要完成一些复杂的、高级的性能分析任务,编译器就需要消耗更多的资源。使用了C2编译器的JVM会消耗更多的资源,例如更多的线程,更多的CPU指令周期,以及更大的code cache等。

    层次编译

    层次编译综合了服务器端编译器和客户端编译器的特点。Azul首先在其Zing JVM中实现了层次编译。最近(就是Java SE 7版本),Oracle Java HotSpot VM也采用了这种设计。在应用程序启动阶段,客户端编译器最为活跃,执行一些由较低的性能计数器阈值出发的性能优化任务。此外,客户端编译器还会插入性能计数器,为一些更复杂的性能优化任务准备指令集,这些任务将在后续的阶段中由服务器端编译器完成。层次编译可以更有效的利用资源,因为编译器在执行一些对应用程序影响较小的编译活动时仍可以继续收集运行时信息,而这些信息可以在将来用于完成更高级的优化任务。使用层次编译可以比解释性的代码性能计数器手机到更多的信息。

    Figure 1中展示了纯解释运行、客户端模式运行、服务器端模式运行和层次编译模式运行下性能之间的区别。X轴表示运行时间(单位时间)Y轴表示性能(每单位时间内的操作数)。

    Figure 1. Performance differences between compilers (click to enlarge)

    编译性能对比

    相比于纯解释运行的的代码,以客户端模式编译运行的代码在性能(指单位时间执行的操作)上可以达到约5到10倍,因此而提升了应用程序的运行性能。其间的区别主要在于编译器的效率、编译器所作的优化,以及应用程序在设计实现时针对目标平台做了何种程度的优化。实际上,最后一条不在Java程序员的考虑之列。

    相比于客户端编译器,使用服务器端编译器通常会有30%到50%的性能提升。在大多数情况下,这种程度的性能提升足以弥补使用服务器端编译所带来的额外资源消耗。

    层次编译综合了服务器端编译器和客户端编译器的优点,使用客户端编译模式实现快速启动和快速优化,使用服务器端编译模式在后续的执行周期中完成高级优化的编译任务。

    常用编译优化手段

    到目前为止,已经介绍了优化代码的价值,以及常用JVM编译器是如何以及何时编译代码的。接下来,将用一些实际的例子做个总结。JVM所作的性能优化通常在字节码这一层级(或者是更底层的语言表示),但这里我将使用Java编程语言对优化措施进行介绍。在这一节中,我无法涵盖JVM中所作的所有性能优化,相反,我希望可以激发你的兴趣,使你主动挖掘并学习编译器技术中所包含了数百种高级优化技术(参见相关资源)。

    死代码剔除

    死代码剔除指的是,将用于无法被调用的代码,即“死代码”,从源代码中剔除。如果编译器在运行时发现某些指令是不必要的,它会简单的将其从可执行指令集中剔除。例如,在Listing 1中,变量被赋予了确定值,却从未被使用,因此可以在执行时将其完全忽略掉。在字节码这一层级,也就不会有将数值载入到寄存器的操作。没有载入操作意味着可以更少的CPU时间,更好的运行性能,尤其是当这段代码是“热点”代码的时候。

    Listing 1中展示了示例代码,其中被赋予了固定值的代码从未被使用,属于无用不必要的操作。

    Listing 1. Dead code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int timeToScaleMyApp( boolean endlessOfResources) {
       int reArchitect = 24 ;
       int patchByClustering = 15 ;
       int useZing = 2 ;
     
       if (endlessOfResources)
           return reArchitect + useZing;
       else
           return useZing;
    }

    在字节码这一层级,如果变量被载入但从未使用,编译器会检测到并剔除这个死代码,如Listing 2所示。剔除死代码可以节省CPU时间,从而提升应用程序的运行速度。

    Listing 2. The same code following optimization

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int timeToScaleMyApp( boolean endlessOfResources) {
       int reArchitect = 24 ;
       //unnecessary operation removed here...
       int useZing = 2 ;
     
       if (endlessOfResources)
           return reArchitect + useZing;
       else
           return useZing;
    }

    冗余剔除是一种类似的优化手段,通过剔除掉重复的指令来提升应用程序性能。

    内联

    许多优化手段都试图消除机器级跳转指令(例如,x86架构的JMP指令)。跳转指令会修改指令指针寄存器,因此而改变了执行流程。相比于其他汇编指令,跳转指令是一个代价高昂的指令,这也是为什么大多数优化手段会试图减少甚至是消除跳转指令。内联是一种家喻户晓而且好评如潮的优化手段,这是因为跳转指令代价高昂,而内联技术可以将经常调用的、具有不容入口地址的小方法整合到调用方法中。Listing 3到Listing 5中的Java代码展示了使用内联的用法。

    Listing 3. Caller method

    1
    2
    3
    int whenToEvaluateZing( int y) {
       return daysLeft(y) + daysLeft( 0 ) + daysLeft(y+ 1 );
    }

    Listing 4. Called method

    1
    2
    3
    4
    5
    6
    int daysLeft( int x){
       if (x == 0 )
          return 0 ;
       else
          return x - 1 ;
    }

    Listing 5. Inlined method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int whenToEvaluateZing( int y){
       int temp = 0 ;
     
       if (y == 0 ) temp += 0 ; else temp += y - 1 ;
       if ( 0 == 0 ) temp += 0 ; else temp += 0 - 1 ;
       if (y+ 1 == 0 ) temp += 0 ; else temp += (y + 1 ) - 1 ;
     
       return temp;
    }

    在Listing 3到Listing 5的代码中,展示了将调用3次小方法进行内联的示例,这里我们认为使用内联比跳转有更多的优势。

    如果被内联的方法本身就很少被调用的话,那么使用内联也没什么意义,但是对频繁调用的“热点”方法进行内联在性能上会有很大的提升。此外,经过内联处理后,就可以对内联后的代码进行进一步的优化,正如Listing 6中所展示的那样。

    Listing 6. After inlining, more optimizations can be applied

    1
    2
    3
    4
    5
    int whenToEvaluateZing( int y){
       if (y == 0 ) return y;
       else if (y == - 1 ) return y - 1 ;
       else return y + y - 1 ;
    }

    循环优化

    当涉及到需要减少执行循环时的性能损耗时,循环优化起着举足轻重的作用。执行循环时的性能损耗包括代价高昂的跳转操作,大量的条件检查,和未经优化的指令流水线(即引起CPU空操作或额外周期的指令序列)等。循环优化可以分为很多种,在各种优化手段中占有重要比重。其中值得注意的包括以下几种:

    • 合并循环:当两个相邻循环的迭代次数相同时,编译器会尝试将两个循环体进行合并。当两个循环体中没有相互引用的情况,即各自独立时,可以同时执行(并行执行)。
    • 反转循环:基本上将就是用do-while循环体换掉常规的while循环,这个do-while循环嵌套在if语句块中。这个替换操作可以节省两次跳转操作,但是,会增加一个条件检查的操作,因此增加的代码量。这种优化方式完美的展示了以少量增加代码量为代价换取较大性能的提升 —— 编译器需要在运行时需要权衡这种得与失,并制定编译策略。
    • 分块循环:重新组织循环体,以便迭代数据块时,便于缓存的应用。
    • 展开循环:减少判断循环条件和跳转的次数。你可以将之理解为将一些迭代的循环体“内联”到一起,而无需跨越循环条件。展开循环是有风险的,它有可能会降低应用程序的运行性能,因为它会影响流水线的运行,导致产生了冗余指令。再强调一遍,展开循环是编译器在运行时根据各种信息来决定是否使用的优化手段,如果有足够的收益的话,那么即使有些性能损耗也是值得的。

    至此,已经简要介绍了编译器对字节码层级(以及更底层)进行优化,以提升应用程序在目标平台的执行性能的几种方式。这里介绍的几种优化手段是比较常用的几种,只是众多优化技术中的几种。在介绍优化方法时配以简单示例和相关解释,希望可以洗发你进行深度探索的兴趣。更多相关内容请参见相关资源。

    总结:回顾

    为满足不同需要而使用不同的编译器。

    • 解释是将字节码转换为本地机器指令的最简单方式,其工作方式是基于对本地机器指令表的查找。
    • 编译器可以基于性能计数器进行性能优化,但是需要消耗更多的资源(如code cache,优化线程等)。
    • 相比于纯解释执行代码,客户端编译器可以将应用程序的执行性能提升一个数量级(约5到10倍)。
    • 相比于客户端编译器,服务器端编译器可以将应用程序的执行性能提升30%到50%,但会消耗更多的资源。
    • 层次编译综合了客户端编译器和服务器端编译器的优点,既可以像客户端编译器那样快速启动,又可以像服务器端编译器那样,在长时间收集运行时信息的基础上,优化应用程序的性能。

    目前,已经出现了很多代码优化的手段。对编译器来说,一个主要的任务就是分析所有的可能性,权衡使用某种优化手段的利弊,在此基础上编译代码,优化应用程序的性能。

    关于作者

    Eva Andearsson对JVM计数、SOA、云计算和其他企业级中间件解决方案有着10多年的从业经验。在2001年,她以JRockit JVM开发者的身份加盟了创业公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收领域的研究和算法方面,EVA获得了两项专利。此外她还是提出了确定性垃圾回收(Deterministic Garbage Collection),后来形成了JRockit实时系统(JRockit Real Time)。在技术上,Eva与SUn公司和Intel公司合作密切,涉及到很多将JRockit产品线、WebLogic和Coherence整合的项目。2009年,Eva加盟了Azul System公,担任产品经理。负责新的Zing Java平台的开发工作。最近,她改换门庭,以高级产品经理的身份加盟Cloudera公司,负责管理Cloudera公司Hadoop分布式系统,致力于高扩展性、分布式数据处理框架的开发。

    相关资源

      • “JVM性能优化, Part 1 ——JVM简介”(原文作者Eva Andreasson, 于2012年8约发表于JavaWorld)是该系列的第一篇,对经典JVM的工作原理做了简单介绍,包括Java“一次编写,到处运行”的优势,垃圾回收基础和一些常用的垃圾回收算法。
      • 更多有关HotSpot优化原理以及JVM热身的内容请参见Vladimir Roubtsov与2003年4约发表于JavaWorld.com的文章“Watch your HotSpot compiler go”
      • 如果你想对JVM和字节码有更深入的了解,请参见Bill Venners在1996年发表于JavaWorld的文章“Bytecode basics”。文章对JVM中的字节码指令集做了介绍,内容包括原生类型操作、类型转换以及栈上操作等。
      • 在Java平台的官方文档中有对Java编译器javac的详细描述。
      • 更多有关JVM中JIT编译器的内容,请参见IBM Research中有关Java JIT Compiler的内容。
      • 或者参见Oracle JRockit文档中“Understanding Just-In-Time Compilation and Optimization”的相关内容.
      • Cliff Click博士在其博客上有关于层次编译的完整教程。
      • 更多有关使用性能计数器完成JVM性能优化的文章:“Using Platform-Specific Performance Counters for Dynamic Compilation” (作者Florian Schneider与Thomas R. Gross;由ACM Digital Lirary发表在第18届Languages and Compilers for Parallel Computing会议上)
      • Oracle JRockit: The Definitive Guide (Marcus Hirt, Marcus Lagergren; Packt Publishing, 2010): Oracle JRockit权威指南

     

    原文链接:  javaworld  翻译:  ImportNew.com  曹旭东
    译文链接:  http://www.importnew.com/2009.html
    展开全文
  • 编译器的三级优化

    2016-11-11 22:44:05
    编译器的三级优化
  • String类型编译器优化

    2017-03-17 09:34:51
    在字符串相加中,只要有一个是非final类型的变量,编译器就不会优化,因为这样的变量可能发生改变,所以编译器不可能将这样的变量替换成常量。例如System.out.println((b+e)==MESSAGE); 结果又变成了false。这也就...
  • 定义一个方法的格式 main方法的外边,class的里面~ public static void 名称(){ } 注意事项: ...对于byte/short/char三种类型,如果右侧赋值的数值没有超过范围,那么javac编译器将自动隐含...
  • 关于优化,可以说途径千千万万,没有一定的法则,但大抵有那么几个比较基本的法则比如,充分了解计算机的体系结构,了解自己所处理的数据的特点,熟悉编译器的工作原理等等。今天我想讲一讲关于编译器方面的一些知识...
  • 上节主要介绍在资源受限的ARM设备上,在各种类型的操作系统上的选择,在C语言编程角度,如何构建代码才能更好的指导编译器compiler进行优化,诸如数据对齐data alignment,数据类型data type的选择,C语言函数调用的...
  • 作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见“JVM性能优化,Part 1″中对JVM的介绍)。Eva Andreasson将对不同种类的编译器做介绍,并比较...
  • GNU编译器提供-O选项供程序优化使用: -O 提供基础级别的优化 -O2 提供更加高级的代码优化,会占用更长的编译时间 -O3 提供最高级的代码优化 -O4 不优化,这是默认值 第一级:代码调整 代码调整是一种局部的思维方式;...
  • 编译器

    2011-11-02 22:24:46
    简单讲,编译器就是将“高级语言”翻译为“机器语言(低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 汇编程序 (assembler) → 目标...
  • JVM性能优化 Part II:编译器    作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见“JVM性能优化,Part 1″中对JVM的介绍)。Eva Andreasson将...
  •  作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见“JVM性能优化,Part 1″中对JVM的介绍)。Eva Andreasson将对不同种类的编译器做介绍,并比较...
  • JVM 性能优化,第二部分:编译器 JVM性能优化,第二部分:编译器 —为你的应用程序选择正确的Java编译器 原文连接译者:Vitas 本文将是JVM 性能优化系列的第二篇文章,Java 编译器将是本文讨论的核心内容。 ...
  • Java—String类型及编译器优化 我们先来看一道题目: public class StringTest{ public static void main(String[] args){ String str = "xiyou" + "3g" + "backend"; ...
  • 编译器和解释器的协调工作流程: 判断是否是热点数据,不是逐条解释成机器码执行,否则一次性编译成机器码存到方法区,以后每次直接运行机器码 在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器...
  • 简介:欢迎走进阿里云机器学习PAI AICompiler编译器系列。随着AI模型结构的快速演化,底层计算硬件的层出不穷,用户使用习惯的推陈出新,单纯基于手工优化来解决AI模型的性能和效率问题越来越容易出现瓶颈。为了应对...
  • 编译器优化奠基人:John Cocke

    千次阅读 2005-12-14 16:00:00
    编译器优化奠基人:John Cocke “IBM小子”,是RISC(Reduced Instruction Set Computer,精简指令系统计算机)架构设计师——John Cocke,在1972年得到的IBM公司颁赠给内部员工的最高荣誉称号。同年,John Cocke还...
  • 作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见“JVM性能优化,Part 1″中对JVM的介绍)。Eva Andreasson将对不同种类的编译器做介绍,并比较...
  • 各种编译器有什么特点和不足

    千次阅读 2017-04-12 21:42:22
    1.MSVC,Windows平台上最常用的编译器,在C++编译器圣战中的胜利者,一个常被人诟病的是对标准的支持不够新不够快(最近开始逐步加快了)。随着微软发布基于Clang / C2,这一条路以后若成功,MSVC与Clang / C2并行,...
  • 这篇文章主要介绍了Java虚拟机JVM性能优化(二):编译器,本文先是讲解了不同种类的编译器,并对客户端编译,服务器端编译器和多层编译的运行性能进行了对比,然后给出了几种常见的JVM优化方法,需要的朋友可以参考下 ...
  • 第一点优化 对于byte/short/char三种类型来说,如果右侧赋值的数值没有超过范围, 那么javac编译器将会自动隐含地为我们补上一个(byte)(short)(char)。 如果没有超过左侧的范围,编译器补上强转。 如果右侧...
  • 编译器三级优化都干了什么?

    千次阅读 2016-11-12 17:05:41
    GNU编译器提供-O选项供程序优化使用: -O 提供基础级别的优化 -O2 提供更加高级的代码优化,会占用更长的编译时间 -O3 提供最高级的代码优化 -O4 不优化,这是默认值 不同的优化级别使用的优化技术也...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 61,691
精华内容 24,676
关键字:

优化编译器的特点