更快的AtomicInteger
感谢同事【空蒙】的投稿
之前看了java8的longadder实现,最近又看到一篇文章介绍longadder实现的。其实现思路也是分段,最后需要get的时候,再进行sum计算。其核心思路就是减少并发,但之前老的Atomic,难道就没有提升的空间了吗?昨晚进行了一次测试。测试代码如下:
[code lang=”java”]
/**
* Atomically increments by one the current value.
*
*@return the updated value
*/
public final int incrementAndGet() {
for(;;) {
int current = get();
int next = current + 1;
if(compareAndSet(current, next))
return next;
}
}
[/code]
以incrementAndGet为例,在非常高的并发下,compareAndSet会很大概率失败,因此导致了此处cpu不断的自旋,对cpu资源的浪费
既然知道此地是高并发的瓶颈,有什么办法呢?
[code lang=”java”]
public class AtomicBetter {
AtomicInteger ai=new AtomicInteger();
public int incrementAndGet() {
for(;;) {
int current =ai.get();
int next = current + 1;
if(compareAndSet(current, next))
return next;
}
}
/**
*如果cas失败,线程park
*@paramcurrent
*@paramnext
*@return
*/
private boolean compareAndSet(intcurrent,intnext) {
if(ai.compareAndSet(current, next)) {
return true;
}else{
LockSupport.parkNanos(1);
return false;
}
}
}
[/code]
很简单,当cas失败后,对线程park,减少多线程竞争导致的频繁cas失败,更进一步的导致cpu自旋,浪费cpu的运算能力。在4核虚拟机,Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz linux 2.6.32,(注意,老版本的内核,不支持高的精度ns级) 进行测试,同样都起4个线程,每个线程里面对AtomicInteger进行5kw次的incrementAndGet。原生的AtomicInteger,耗时14232ms,进行了35870次上下文切换,总共87967770955次时钟周期。那prak 1ns下呢,耗时5195ms,进行了19779次上下文切换,总共36187480878次时钟周期,明显性能上比原生的AtomicInteger更好,那这个park多少合适呢?那就只有人肉测试了
park | time(ms) | context-switches | cycles |
AtomicInteger | 14232 | 35,870 | 87,967,770,955 |
1ns | 5195 | 19,779 | 36,187,480,878 |
10ns | 5050 | 20,223 | 34,839,351,263 |
100ns | 5238 | 20,724 | 37,250,431,417 |
125ns | 4536 | 47,479 | 26,149,046,788 |
140ns | 4008 | 100,022 | 18,342,728,517 |
150ns | 3864 | 110,720 | 16,146,816,453 |
200ns | 3561 | 125,694 | 11,793,941,243 |
300ns | 3456 | 127,072 | 10,200,338,988 |
500ns | 3410 | 132,163 | 9,545,542,340 |
1us | 3376 | 134,463 | 9,125,973,290 |
5us | 3383 | 122,795 | 9,009,226,315 |
10us | 3367 | 113,930 | 8,905,263,507 |
100us | 3391 | 50,925 | 8,359,532,733 |
500us | 3456 | 17,225 | 8,096,303,146 |
1ms | 3486 | 10,982 | 7,993,812,198 |
10ms | 3456 | 2,600 | 7,845,610,195 |
100ms | 3555 | 1,020 | 7,804,575,756 |
500ms | 3854 | 822 | 7,814,209,077 |
本机环境下,park 1ms下,相对耗时,cs次数来说是最好的。因此这种优化要达到最佳效果,还要看cpu的情况而定,不是一概而定的
两个问题:
1、cas失败对线程park的副作用是什么。
2、如果park的时间继续加大,那会是这么样的结果呢。
原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 更快的AtomicInteger
我觉得将park的时间增大后,再看结果还是很有必要的。
另外我觉得park的时间与实际环境有很大关系,线程个数,操作次数等等,单纯的park某个时间不是很合适,不知道我的想法是否正确。
如有错误,期待与您交流,我的邮箱:casinww@163.com
我同意您的观点。
另外一个疑问:
“本机环境下,park 1ms下,相对耗时,cs次数来说是最好的。因此这种优化要达到最佳效果,还要看cpu的情况而定,不是一概而定的“
既然和具体的cpu有关,那么这个设计是不是可以理解为非可移植性的呢?(另外,我认为很和并发程度也是相关的,文中没有测试低并发场景下的性能,这样的实验一般是控制读写比例和控制并发线程数目来做,读写比例:比方说20%increment 10%decrement 70%get对比50%increment 50%decrement)
或者说,如果要做到可移植性,那么需要有公式来计算这个park的时间?
谢谢!
我的疑问是,自旋不就是为了减少上下文切换吗? 为什么park 1ns 后反而上下文次数减少这么多?
increment 和 decrement,最下面都是公共的unsafe的cas,不管怎么分配比例,所以其实是一样的,而且increment本身就是int返回值,所以也没有额外再去get的需要,就是get也只是直接返回里面的volatile int value。本身这个调整,就是减少cas时候自旋的开销,这算有些线程主动牺牲一下的雷锋精神吧。这测试和硬件环境与并发数是有很大关联,但应该可选取到一个较合适的park时间,以获取最好效果吧
自旋目的,确实是为减少上下文切换,达到自增(减)完成,这在低并发下确实,但线程也受内核的调度,如果高并发下很多线程在自旋,linux2.6.32下,cfs也会动态调度,自然也有线程切换开销。
哦,对,原来脑袋里的印象一直是 自旋,自旋,却没想到自旋的线程本身也是受OS调度的,并且,自旋导致线程的占用时间片增多,在调度算法上 可能还会受到惩罚。 恩,跳出来,又学习到一点东西。
你这种方式性能提升了一倍?从开头那文章里给出的性能对比数据看,你这种方式性能提升的并不算多
sun lab里有人针对CAS的性能做过调整测试,还写了篇论文,其中一个方法就是文章使用的,在cas失败时进行一定时间的等待。
论文里,也没有详细的解释这个做法能起到效果的原因。说了一些,觉得大概意思是说多核cpu,在高并发的情况下,cas失败率会显著提升,这些也会增加因为要保持各个处理器的缓存,主存一致性同步的操作,而这些失败的cas去取无效的值,也没什么意义,同时也会导致成功的cas变慢。那么,适当地降低并发,就能达到效果。
至于参数,他们也还没有找到平台独立的策略。
论文链接
Lightweight Contention Management for Efficient Compare-and-Swap Operations
https://blogs.oracle.com/dave/entry/lightweight_contention_management_for_efficient
这个测试数据是怎么获取
求回复,测试数据怎么抓取出来的??有github地址么
return next;
这里错了是return current;