翻译人员: 铁锚

翻译时间: 2013年11月3日

原文链接: Efficient Counter in Java

我们经常使用 HashMap作为计数器(counter)来统计数据库或者文本中的某些东西.

本文将使用HashMap来实现计数器的3种不同方式进行对比。

1. 新手级计数器

如果使用这一类别的计数器,那么代码大致如下所示:

String source = "my name is name me and your name is her first her";

String[] words = source.split(" ");

// 新手级计数器

public static void testNaive(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

int oldValue = counter.get(w);

counter.put(w, oldValue+1);

} else {

counter.put(w, 1);

}

}

}

在每次循环中,判断是否包含了相应的key,如果包含,那么值在原来的基础上加1,如果没有,那就设置为1.

此种方式简单又直接,但并不是很有效率。效率不高的原因如下:

1.1 当一个key存在时,containsKey() 和 get() 分别调用了一次,这意味着对map进行了两次查找。

1.2 因为 Integer 是不可变的,每次循环在增加计数值的时候将会创建一个新的对象.

2. 入门级计数器

那么我们自然需要使用一个可变的整数来避免创建太多个Integer对象.可变整数类可以如下面所示来定义:

// 可变Integer

public static final class MutableInteger{

private int val;

public MutableInteger(int val){

this.val = val;

}

public int get(){

return this.val;

}

public void set(int val){

this.val = val;

}

// 为了方便打印

public String toString() {

return Integer.toString(val);

}

}

那么计数器可以用如下的方式来改进:

// 入门级计数器

public static void testBetter(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

MutableInteger oldValue = counter.get(w);

oldValue.set(oldValue.get()+1); // 因为是引用,所以减少了一次HashMap查找

} else {

counter.put(w, new MutableInteger(1));

}

}

}

因为不需要创建太多的Integer对象,看起来好了一些。然而,key存在的情况下,每次循环依然要进行两次查找.

3. 卓越级计数器

HashMap 的 put(key,value) 方法会返回key对应的当前value.了解这个特性,我们可以利用原有值来进行递增,并不需要多次的查找.

public static void testEfficient(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

MutableInteger initValue = new MutableInteger(1);

// 利用 HashMap 的put方法弹出旧值的特性

MutableInteger oldValue = counter.put(w, initValue);

if(oldValue != null){

initValue.set(oldValue.get() + 1);

}

}

}

4. 性能差异

为了测试这三种实现方式的性能,采用了下面的代码。先看看结果如何,性能测试分别执行了多次,对每一个数量级的测试,误差不算太大,所以取其中的一个结果排列如下:

10000000 次循环:

新手级计数器: 7726594902

入门级计数器: 6516014840

卓越级计数器: 5736574103

1000000 次循环:

新手级计数器: 777480106

入门级计数器: 642932000

卓越级计数器: 571867738

100000 次循环:

新手级计数器: 84323682

入门级计数器: 70176906

卓越级计数器: 61219664

10000 次循环:

新手级计数器: 13279550

入门级计数器: 7874100

卓越级计数器: 6460172

1000 次循环:

新手级计数器: 4542172

入门级计数器: 2933248

卓越级计数器: 992749

100 次循环:

新手级计数器: 3092325

入门级计数器: 1101695

卓越级计数器: 423942

10 次循环:

新手级计数器: 1993788

入门级计数器: 558150

卓越级计数器: 153156

1 次循环:

新手级计数器: 1625898

入门级计数器: 427494

卓越级计数器: 69473

从上面的输出可以看到,10000次的时候, 13:8:6 秒,相差很明显.特别是 新手级计数器和入门级计数器之间的比例,这说明创建对象是很耗资源的操作。

当然,次数更多的差距不明显的原因在于,触发了多次的GC垃圾回收,同时也证明了垃圾回收的代价确实很大。

完整的测试代码如下:

import java.util.HashMap;

public class TestCounter {

public static void main(String[] args) {

// 源字符串

String source = "my name is name me and your name is her first her";

// 计时,单位: 微秒

long startTime = 0;

long endTime = 0;

long duration = 0;

// 测试次数

int loop = 1 * 10000;

System.out.println(loop +" 次循环:");

startTime = System.nanoTime();

testNaive(source,loop);

endTime = System.nanoTime();

duration = endTime - startTime;

System.out.println("新手级计数器: " + duration);

//

startTime = System.nanoTime();

testBetter(source, loop);

endTime = System.nanoTime();

duration = endTime - startTime;

System.out.println("入门级计数器: " + duration);

//

startTime = System.nanoTime();

testEfficient(source, loop);

endTime = System.nanoTime();

duration = endTime - startTime;

System.out.println("卓越级计数器: " + duration);

}

// 新手级计数器

public static void testNaive(String source, int loop){

if(null == source){

return;

}

//

String[] words = source.split(" ");

for (int i = 0; i

testNaive(words);

}

}

public static void testNaive(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

int oldValue = counter.get(w);

counter.put(w, oldValue+1);

} else {

counter.put(w, 1);

}

}

}

// 可变Integer

public static final class MutableInteger{

private int val;

public MutableInteger(int val){

this.val = val;

}

public int get(){

return this.val;

}

public void set(int val){

this.val = val;

}

// 为了方便打印

public String toString() {

return Integer.toString(val);

}

}

// 入门级计数器

public static void testBetter(String source, int loop){

if(null == source){

return;

}

//

String[] words = source.split(" ");

for (int i = 0; i

testBetter(words);

}

}

public static void testBetter(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

MutableInteger oldValue = counter.get(w);

oldValue.set(oldValue.get()+1); // 因为是引用,所以减少了一次HashMap查找

} else {

counter.put(w, new MutableInteger(1));

}

}

}

// 卓越级计数器

public static void testEfficient(String source, int loop){

if(null == source){

return;

}

//

String[] words = source.split(" ");

for (int i = 0; i

testEfficient(words);

}

}

public static void testEfficient(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

MutableInteger initValue = new MutableInteger(1);

// 利用 HashMap 的put方法弹出旧值的特性

MutableInteger oldValue = counter.put(w, initValue);

if(oldValue != null){

initValue.set(oldValue.get() + 1);

}

}

}

}

5. Keith网站评论列表

我觉得最好的评论如下:

添加了三个测试:

1) 重构了 “入门级计数器”,不使用containsKey,改为只使用get方法. 通常你需要的元素是存在于 HashMap 中的, 所以将 2 次查找精简为 1次.

2) 作者 michal 提到过的方式,使用 AtomicInteger来实现 .

3) 使用单个的int 数组来进行对比,可以使用更少的内存,参见 http://amzn.com/0748614079

我运行了测试程序3次,并挑选出最小的那个值(以减少干扰). 注意: 你不能在程序中让运行结果受到太多干扰,因为内存不足可能会受到gc垃圾回收器太多的影响.

新手级计数器: 201716122

入门级计数器: 112259166

卓越级计数器: 93066471

入门级计数器 (不使用 containsKey): 69578496

入门级计数器 (不使用 containsKey, with AtomicInteger): 94313287

入门级计数器 (不使用 containsKey, with int[]): 65877234

入门级计数器 (不使用 containsKey 方法:):

HashMap efficientCounter2 = new HashMap();

for (int i = 0; i

for (String a : sArr) {

MutableInteger value = efficientCounter2.get(a);

if (value != null) {

value.set(value.get() + 1);

}

else {

efficientCounter2.put(a, new MutableInteger(1));

}

}

入门级计数器 (不使用 containsKey, 使用 AtomicInteger):

HashMap atomicCounter = new HashMap();

for (int i = 0; i

for (String a : sArr) {

AtomicInteger value = atomicCounter.get(a);

if (value != null) {

value.incrementAndGet();

}

else {

atomicCounter.put(a, new AtomicInteger(1));

}

}

入门级计数器 (不使用 containsKey, 使用  int[]):

HashMap intCounter = new HashMap();

for (int i = 0; i

for (String a : sArr) {

int[] valueWrapper = intCounter.get(a);

if (valueWrapper == null) {

intCounter.put(a, new int[] { 1 });

}

else {

valueWrapper[0]++;

}

}

Guava 语言的 MultiSet 可能更快一些.

6. 结论

优胜者是使用int数组的方式.

参考文章

随机计数器java_Java高效计数器相关推荐

  1. 陈键飞:基于随机量化的高效神经网络训练理论及算法

    [专栏:前沿进展]随着预训练模型参数规模的增长,所需的算力也不断增加,从算法层面研究和处理模型规模的增长成为研究者关注的话题.近期举办的Big Model Meetup第二期活动,特邀清华大学助理教授 ...

  2. 【Linux 内核 内存管理】Linux 内核堆内存管理 ③ ( CPU 计数器瓶颈 | per-CPU 计数器 | Linux 内核 percpu_counter 结构体源码 )

    文章目录 一.CPU 计数器瓶颈 二.per-CPU 计数器及 percpu_counter 结构体源码 一.CPU 计数器瓶颈 如果 操作系统 中有 多个 CPU , 假设只有一个 CPU 计数器工 ...

  3. 用74ls90组成二十四进制计数器_减法计数器的组成以及原理

    异步二进制减法计数器如图1-1所示 减法计数器的结构原理 1-1减法计数器的结构原理 该计数器是一个3位二进制异步减法计数器,它与前面介绍过的3位二进制异步加法计 数器一样,是由3个JK触发器组成,其 ...

  4. 使用Verilog语言描述计数器——脉动计数器;脉动计数器具有减法计数功能。采用模块设计和行为级设计方法。

    使用Verilog语言描述计数器--脉动计数器. 内容说明: 本次设计的计数器属于脉动计数器.使用Verilog语言设计,并且设计方法采用模块设计和简单的行为级设计.会有这两种设计的对比测试.最后,会 ...

  5. 同步计数器与异步计数器的区别,以及4040计数器的使用

    目录 一.4040计数器原理 二.分类 1.异步计数器与同步计数器 三.应用 一.4040计数器原理 4040是由T触发器组成的而精致计数器,主要用于分频和计数. 4040内部有12个计数级,每个计数 ...

  6. verilog实现二进制计数器,约翰逊(Johnson)计数器,环形计数器

    https://github.com/zsylov/verliog-study/blob/master/2019.5.6%E8%AE%A1%E6%95%B0%E5%99%A8.md 用verilog实 ...

  7. 七种计数器总结(格雷码计数器、环形计数器、约翰逊计数器、FLSR、简易时分秒数字秒表|verilog代码|Testbench|仿真结果)

    七种计数器总结 一.可复位/置数计数器 1.1 可复位/置数计数器 1.2 Verilog代码 1.3 Testbench 1.4 仿真结果 二.双向(可加可减)计数器 2.1 双向(可加可减)计数器 ...

  8. 分别设计网页访问计数器,会话计数器,访问网站计数器。

    张继军 董卫 <java web 应用开发技术与实案列教程>课后习题之第三章第7题 网页访问计数器count1.jsp 会话计数器count2.jsp 访问网站访问计数器count3.js ...

  9. 74192减法计数器原理图_趣味学习三菱PLC之定时器和计数器|定时器|三菱汽车|计数器|继电器|plc...

    小时候总想着,自己要是可以控制时间就好了,给时间按下暂停键,然后把班里的那个死对头打一顿哈哈哈哈哈嗝,做梦呢.虽然我不可以控制时间,但是我可以通过定时器控制PLC的程序执行呀,这也是从另一方面实现我控 ...

  10. 随机计数器java_Java——随机计数器

    /* * Copyright (c) 2014, 烟台大学计算机学院 * All rights reserved. * 文件名称:test.cpp * 作    者:李晓凯 * 完成日期:2015年 ...

最新文章

  1. R语言使用caret包中的createFolds函数对机器学习数据集进行交叉验证抽样、返回的样本列表长度为k个
  2. 可能是目前最给力的开源硬件——ESPlay Micro V2,童芯派劲敌他来了
  3. canal应用一:基于mysql binlog的日志解析工具
  4. 找出两列表共有的元素python,两个列表之间的公共元素未在Python中使用集
  5. 分布式文件系统之ceph是什么?
  6. Python 100例(上)
  7. 使用HTML5 IndexDB存储图像和文件
  8. C++多态相关关问题及虚表剖析
  9. SLF4JLogFactory does not implement org.apache.commons.logging.LogFactory
  10. 黄聪:is_file和file_exists效率比较
  11. [C++]变量和基本类型
  12. exec和source的区别
  13. 基于WEKA实现时间序列的预测
  14. 如何使用PS将图片中的类千图网的字眼去掉
  15. zabbix官网下载地址:https://sourceforge.net/projects/zabbix/files/ZABBIX Latest Stable/
  16. 人工智能会取代艺术家?
  17. pgsql之create user与create role的区别
  18. 在游戏策划中应用SCAMPER创新
  19. spring boot接入微信小程序支付流程
  20. java日志:三、JCL使用

热门文章

  1. 给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
  2. 【洛谷 1057】传球游戏
  3. Js/jQuery实时监听input输入框值变化
  4. 直接选择排序(Straight Selection Sort)
  5. RUBY常用类库文档翻译以及使用示例
  6. 安装8in1飞行模拟器过程
  7. Python GUI程序整理
  8. 智能优化算法:金枪鱼群优化算法-附代码
  9. 从零基础入门Tensorflow2.0 ----三、11. tf.GradientTape与tf.keras结合使用
  10. EO CAT软件下载数据