Java高效计数器

翻译人员: 铁锚
翻译时间: 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<String, Integer> counter = new HashMap<String, Integer>();
	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<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
	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<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
	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 < loop; i++) {
			testNaive(words);
		}
	}
	public static void testNaive(String[] words){
		HashMap<String, Integer> counter = new HashMap<String, Integer>();
		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 < loop; i++) {
			testBetter(words);
		}
	}
	public static void testBetter(String[] words){
		HashMap<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
		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 < loop; i++) {
			testEfficient(words);
		}
	}
	public static void testEfficient(String[] words){
		HashMap<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
		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);
			}
		}
	}
}

当你实用计数器的时候,很可能也需要根据值来进行排序的方法,请参考: the frequently used method of HashMap.


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<string, mutableinteger=""> efficientCounter2 = new HashMap<string, mutableinteger="">();
for (int i = 0; i < NUM_ITERATIONS; 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<string, atomicinteger=""> atomicCounter = new HashMap<string, atomicinteger="">();
for (int i = 0; i < NUM_ITERATIONS; 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<string, int[]=""> intCounter = new HashMap<string, int[]="">();
for (int i = 0; i < NUM_ITERATIONS; 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数组的方式.


参考文章

1. Most efficient way to increment a Map value in Java.

2. HashMap.put() HashMap.put()

相关阅读
1. ArrayList vs. LinkedList vs. Vector

2. HashSet vs. TreeSet vs. LinkedHashSet

3. Frequently Used Methods of Java HashMap

4. Java Convert Hashtable to Treemap


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页