三个常见的代码性能优化方式

栏目: 编程工具 · 发布时间: 6年前

内容简介:编写有效率的代码是我们的一项基本技能。我们千万不要忽视代码的性能要求。越早考虑性能问题,需要支付的成本就越小,带来的价值就越大,不要等到出现性能问题时,才去临时抱佛脚。如果前期没有看重代码的性能问题,那么后期我们就要付出加倍的精力去维护和重构代码。代码的性能并不是可以多块地进行加减乘除,而是如何管理内存、磁盘、网络、内核等计算机资源,已达到性能最优化。在这篇文章里,我选了三个常见且实用的代码性能优化方式,供你参考和借鉴。

编写有效率的代码是我们的一项基本技能。我们千万不要忽视代码的性能要求。越早考虑性能问题,需要支付的成本就越小,带来的价值就越大,不要等到出现性能问题时,才去临时抱佛脚。如果前期没有看重代码的性能问题,那么后期我们就要付出加倍的精力去维护和重构代码。

代码的性能并不是可以多块地进行加减乘除,而是如何管理内存、磁盘、网络、内核等计算机资源,已达到性能最优化。

在这篇文章里,我选了三个常见且实用的代码性能优化方式,供你参考和借鉴。

让接口保持简单直观的两个小技巧

设计接口之所以难,在于接口对稳定性的要求比较高。要想保证接口的稳定性,最有效的方法就是让接口设计得简单直观一些。在工作中,我总结了两个小技巧供你参考使用。

学会拆解问题

设计软件接口,要从实际的问题出发,只有这样,我们才能找到一条清晰的主线。围绕这条主线展开设计,就可以有效地避免需求膨胀和过渡设计。

拆解问题,需要遵循两个原则—— 相互独立,完全穷尽。

比如说,是否可以授权一个用户使用某一个在线服务呢?这个问题就可以分解为两个小问题:

1、该用户是否为已注册的用户?

2、该用户是否持有正确的密码?

我们可以使用思维导图来描述这个分解。

三个常见的代码性能优化方式

这种划分其实是有问题的。因为只有已经注册的用户,才会持有正确的密码。而且,只有持有正确密码的用户,才能够被看作是注册用户。这两个小问题之间,存在着依赖关系,就不能算是“相互独立”。

我们要消除掉这种依赖关系,这样需要两个层次的表达。第一个层次问题是,该用户是否为已注册的用户?这个问题,可以进一步分解为两个更小的问题:用户持有的用户名是否已注册? 用户持有的密码是否匹配?

1、该用户是否是已注册的用户?

用户名是否已注册?

用户密码是否正确?

三个常见的代码性能优化方式

但这样还是有缺陷的。如果一个服务,对所有的注册用户开放,上面的分解就是完备的。否则,我们就漏掉了一个重要的内容,不同的注册用户,可以访问的服务可能是不同的。也就是说如果没有访问的权限,那么即使用户名和密码正确也无法访问相关的服务。

如果我们把漏掉的加上,这个问题的分解可以进一步表示为:

1、该用户是否是已注册的用户?

用户名是否已注册?

用户密码是否正确?

2、该用户是否有访问的权限?

三个常见的代码性能优化方式

到这一步,我们就会有一个清晰的思路了。

一个接口解决一件事情

这里所说的“事情”,其实是在某一个层级上的一个职责,比如授权用户访问是一件完整、独立的事情。有了逻辑级别,我们才能分解问题,接口之间才能建立联系。

对于一件事的划分,我们要注意三点。

1、一件事就是一件事,不是两件事,也不是三件事。

2、这件事是独立的。

3、这件事是完整的。

我们以一段代码为例,看一下如果接口不明确会产生怎样的后果。

复制代码

/**
 * A {@code HelloWords} objectisresponsiblefordetermining how to say
 *"Hello"indifferent language.
 */
classHelloWords{
   privateStringlanguage= "English";
   privateStringgreeting= "Hello";
{1}
    //snipped 
{1}
    /**
     *Setthelanguageofthegreeting.
     *
     * @paramlanguagethelanguageofthegreeting.
     */
   voidsetLanguage(String language){
    //snipped 
    }
{1}
    /**
     *Setthegreetingsofthegreeting.
     *
     * @paramlanguagethegreetingsofthegreeting.
     */
   voidsetGreeting(String greeting){
    //snipped 
    }
{1}
    //snipped 
}
{1}

这段代码涉及两个要素,一个是语言(英语、汉语等),一个是问候语(Hello、你好等),它抽象出了这两个要素。使用 setLanguage() 设置问候的语言,使用 setGreeting() 设置问候的问候语。但这样的设计对用户是不友好的。因为 setLanguage() 和 setGreeting() 这两个方法,都不能表达一个完整的事情。只有两个方法合起来,才能表达一件完整的事情。

这种互相依赖的关系,会导致很多问题。 比如说:

1、使用时,应该先调用哪一个方法?

2、如果语言和问候语不匹配,会出现什么情况?

3、实现时,需不需要匹配语言和问候语?

4、实现时,该怎么匹配语言和问候语?

所以我们应当牢记,接口应该尽可能只解决一件事情,如果实在做不到,就需要减少依赖关系。

想了解更多有关接口设计的内容,请点击: 怎么设计一个简单又直观的接口?

学会使用 JMH,避免性能陷阱

我们如何才能知道自己编写的代码的性能呢?事实上,Java 提供了一个性能测试工具 JMH,它可以直观地帮助我们查看代码的性能缺陷和陷阱。

JMH 的使用方法

首先,使用 Maven 工具建立一个基准测试项目:

复制代码

mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=com.example \
-DartifactId=myJmh \
-Dversion=1.0

然后编译基准测试:

复制代码

cd myJmh
$ mvn clean install

最后运行编译测试:

复制代码

cd myJmh
$ Java -jar target/benchmarks.jar

下面是运行结果,我们需要注意到 Score 这一栏,它显示的是每秒可以执行的基准测试方法的次数。次数越多,效率越高。

复制代码

Benchmark Mode Cnt Score Error Units
MyBenchmark.testMethod thrpt2535.945▒0.694ops/s

下面我们通过运行三个字符串 String、StringBuilder 和 StringBuffer,来看下这三个字符串的性能差异。为了方便对比,JMH 的测试结果,都写在了注释里。

复制代码

// JMH throughput benchmark: about 32 operations per second
@Benchmark
publicStringmeasureStringApend(){
String targetString ="";
for(inti =0; i <10000; i++) {
targetString +="hello";
}

returntargetString;
}

// JMH throughput benchmark: about 5,600 operations per second
@Benchmark
publicStringmeasureStringBufferApend(){
StringBuffer buffer =newStringBuffer();
for(inti =0; i <10000; i++) {
buffer.append("hello");
}

returnbuffer.toString();
}

// JMH throughput benchmark: about 21,000 operations per second
@Benchmark
publicStringmeasureStringBuilderApend(){
StringBuilder builder =newStringBuilder();
for(inti =0; i <10000; i++) {
builder.append("hello");
}

returnbuilder.toString();
}

你可能会看到,使用 String 的性能是最差的,StringBuilder 的字符串连接操作,比使用 String 的操作快了近 200 倍,而 StringBuffer 的字符串连接操作,更是快了近 700 倍。

为什么 String 的效率如此慢?这是因为每一个字符串连接的操作,都需要创建一个新的 String 对象,然后再销毁,再创建。这种模式对 CPU 和内存消耗都比较大。

StringBuilder 为什么比 StringBuffer 还要快呢?StringBuffer 的字符串操作是多线程安全的,而 StringBuilder 的操作就不是。如果我们看这两个方法的实现代码,除了线程安全的同步以外,几乎没有差别。

通过上面的基准测试,我们可以得出这样的结论:

1、频繁的对象创建、销毁,有损代码的效率;

2、减少内存分配、拷贝、释放的频率,可以提高代码的效率;

3、即使是单线程环境,使用线程同步依然有损代码的效率。

但这并不意味着使用 StringBuilder 会更好,想要查看更多基准测试和结论,请点击: 有哪些招惹麻烦的性能陷阱?

超越线程同步的技巧

我们都知道,线程同步有损效率。在实际工作中,我们只要打破下面的任何一个条件,就不需要使用线程同步了:

使用单线程;

1、不关心共享资源的变化;

2、没有改变共享资源的行为。

3、应用到具体的工作场景中,又该怎么避免线程同步呢?

学会使用 final 关键字

Java 里面的 final 关键字,可以把变量改为不可变的量。在软件环境里,不可变,就意味着一旦实例化,就不再改变。

比如下面这段代码是没有使用 final 关键字的。如果只有一个线程,这段代码就没有问题。但是,如果有两个线程,一个线程读,一个线程写,就会出现竞争状况,返回不匹配的语言环境和问候语。

复制代码

classHelloWords {
privateStringlanguage ="English";
privateStringgreeting ="Hello";

voidsetLanguage(Stringlanguage) {
this.language = language;
}

voidsetGreeting(Stringgreeting) {
this.greeting = greeting;
}

StringgetLanguage() {
returnlanguage;
}

StringgetGreeting() {
returngreeting ;
}
}

如果我们使用了 final 关键字,类变量只能被赋值一次,而且只能在实例化之前被赋值。这样的变量,就是不可变的量。如果一个类的所有的变量,都是不可变的,那么这个类也是不可变的。

复制代码

classHelloWords{
privatefinalStringlanguage;
privatefinalStringgreeting;

HelloWords(Stringlanguage,Stringgreeting) {
this.language = language;
this.greeting = greeting;
}

StringgetLanguage() {
returnlanguage;
}

StringgetGreeting() {
returngreeting ;
}
}

所以,我们要养成一个习惯,看到声明的变量,就要琢磨,这个变量能不能声明成不可变的量?有没有办法修改接口设计或者实现代码,把它改成不可变的量?设计一个类时,要优先考虑,这个类是不是可以设计成不可变的类?这样就可以避免很多不必要的线程同步,让代码的效率更高,接口更容易使用。

更多有关超越线程同步的内容,请点击: 高效率,从超越线程同步开始! 进行留言互动。

你在工作中还会遇到哪些有关代码的性能问题?又是如何解决的呢?欢迎你在评论区与我分享你的经验和心得。

文章选自 《代码精进之路》


以上所述就是小编给大家介绍的《三个常见的代码性能优化方式》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Foundations of PEAR

Foundations of PEAR

Good, Nathan A./ Kent, Allan / Springer-Verlag New York Inc / 2006-11 / $ 50.84

PEAR, the PHP Extension and Application Repository, is a bountiful resource for any PHP developer. Within its confines lie the tools that you need to do your job more quickly and efficiently. You need......一起来看看 《Foundations of PEAR》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具