• 首页
  • 关于

我自然

作者存档:yankay

Java使用”指针”快速比较字节

在 2012年3月16日 上公布 作者为 yankay

如何才能快速比较两个字节数组呢?我将问题描述成下面的接口:

 
public int compareTo(byte[] b1, int s1, int l1, byte[] b2, int s2,int l2);

最直观的做法是同时遍历两个数组,两两比较。

 
public int compareTo(byte[] buffer1, int offset1, int length1,
		byte[] buffer2, int offset2, int length2) {
	// Short circuit equal case
	if (buffer1 == buffer2 && offset1 == offset2
		&& length1 == length2) {
		return 0;
	}
	// Bring WritableComparator code local
	int end1 = offset1 + length1;
	int end2 = offset2 + length2;
	for (int i = offset1, j = offset2; i 

如果事情这么简单就结束了,就没有意思了。

如果要提升性能,可以做循环展开等等优化,但这些优化应该依赖JVM来做,新的JVM可以做的很好。那还有什么办法可以提高性能呢?
可以将字节数组合并!!上面的例子中,每个byte被迫转型成了int,再比较。其实我们可以将8个byte转换成一个long,在比较long,这样效果会不会好些?用什么方法转换才是最优的?

 
long sun.misc.Unsafe.getLong(Object o,int offset)

Java提供了一个本地方法,可以最快最好转换byte与long。该函数是直接访问一个对象的内存,内存地址是对象指针加偏移量,返回该地址指向的值。有人说Java很安全,不可以操作指针,所以有的时候性能也不高。其实不对,有了这个Unsafe类,Java一样也不安全。所以Unsafe类中的方法都不是public的,不过没关系,我们有反射。言归正传,下面是使用这种技术手段的实现代码。

 
public int compareTo(byte[] buffer1, int offset1, int length1,
		byte[] buffer2, int offset2, int length2) {
	// Short circuit equal case
	if (buffer1 == buffer2 && offset1 == offset2
			&& length1 == length2) {
		return 0;
	}
	int minLength = Math.min(length1, length2);
	int minWords = minLength / Longs.BYTES;
	int offset1Adj = offset1 + BYTE_ARRAY_BASE_OFFSET;
	int offset2Adj = offset2 + BYTE_ARRAY_BASE_OFFSET;

	/*
	 * Compare 8 bytes at a time. Benchmarking shows comparing 8
	 * bytes at a time is no slower than comparing 4 bytes at a time
	 * even on 32-bit. On the other hand, it is substantially faster
	 * on 64-bit.
	 */
	for (int i = 0; i >> n) & 0xFFL) - ((rw >>> n) & 0xFFL));
		}
	}

	// The epilogue to cover the last (minLength % 8) elements.
	for (int i = minWords * Longs.BYTES; i 

实现比原来复杂了一些。但这次一次可以比较8个字节了。这种getLong函数和系统的字节序是紧紧相关的,如果是小端序操作起来有点麻烦,代码先省略掉。这样操作实际效果如何?我们需要对比测试下。对比两个1M的字节数组,如果使用第一个版本,每次比较平均需要2.5499ms,如果使用第二个版本,需要0.8359ms,提升了3倍。对应这种CPU密集型的操作,这样的提升可是很可观的。

如果要提升性能,使用Unsafe直接访问内存也是不错的选择。

文章分类 软件技术 | 标签: Java, Java字节数组比较, Java字节比较, 字节, 比较 | 发表评论 |

HBase介绍PPT

在 2012年3月14日 上公布 作者为 yankay

Hbase介绍 from kaiyannju
文章分类 软件技术 | 标签: Hbase | 1 条评论 |

Amazon EBS架构猜想

在 2012年3月8日 上公布 作者为 yankay

Amazon EBS是专门为Amazon EC2 虚拟机设计的弹性块存储服务。Amazon EBS可以为Amazon EC2的虚拟机创建卷volumes,Amazon EBS卷类似没有格式化的外部卷设备。卷有设备名称,同时也提供了块设备接口。你可以在Amazon EBS 卷上驻留自己的文件系统,或者直接作为卷设备使用。也就是说EBS就是一个基于集群的完美的“大磁盘”,可以随机读写,较高性能,完美的一致性和高可靠。本来以为这只是幻象,十分不好做。杨公一席话让我茅塞顿开。所以猜测EBS的架构如下,内部人士不要笑话我。

架构

  • EBS虚拟磁盘驱动器:EBS客户端构件,和EC2部署在一起,同生共死。作为操作系统的磁盘驱动器存在。作用是管理该磁盘的块,接受磁盘请求。
  • 缓冲存储:我猜测,如果要想保证高性能,同时保证数据不丢失,需要使用一个本地的持久化存储作为缓冲和缓存。直接对集群进行大量的细碎的操作,延迟是不可接受的。如果使用长连接,多网卡,可以让延迟变得可以接受,那么这个组件就不是必须的。
  • 类S3:类似S3的Key-Value存储。有高可靠,高延时,高吞吐的特点。肯定不是S3,也许也不是Key-value,但是大致是类似的。不可修改和按块存储的特性是相似的。
  • 类Mysql:类似Mysql的DB,用来存储块的信息,必须高可靠,容量不比太大,压力也不大。
  • Slave:可选组件,Slave会记录磁盘驱动器的每个操作,同步其日志。如果EC2那台机器的缓冲存储损坏,可以使用Slave上面的来恢复最近这段时间没有同步到S3的数据。

存储方式

存储的基本单位是块。每个块由Key和Value组成。块的Key分三部分:diskNo-blockNo-version,Value就是Block的内容。块存储在”S3″中,每个块都是不可以修改的,逻辑上的修改通过增加版本来实现。同时不是每一次修改都必须增加版本的。具体方式下面说。块的大小估计在4M左右,要综合”S3″的性能来决定。

一个“盘”由若干个“块”构成,需要记录每一个”块版本号”,blockNo和块的个数在盘创建的时候就已经决定了。“块版本号”信息持久化存储在Mysql中。

这个架构分两层,S3是底层,负责不断存储“盘”的快照。本地缓冲提供低延迟读写。

实现

下面分几种情况,分别来讨论如何实现。

创建,挂载盘

当创建一个盘的时候,只要在Mysql生成一个新的DiskNo。根据盘的大小和Block的大小,计算出Block数量,在Mysql中初始化元信息,将每个Block的版本标记为0(Block在物理上还不存在)。

然后“磁盘驱动器”挂载他,将Mysql中的源信息,加载到内存中。如果上次非正常关闭,可以通过缓冲存储中的数据,执行恢复操作。因为这个盘不是共享的只有该EC2可以使用。所以挂载后不需要再读Mysql.所以对元数据的操作都发生在内存中,每隔一段时间(比如10分钟),将元数据增量添加到Mysql。

有一点需要注意的是,如果一个块在元数据中有,这个块的数据可能在本地缓冲也可能在S3上。但是如果在Mysql的元数据中有,S3上必须存在有该块。

读块

根据blockId,可以得之其最近版本,并且是在本地缓冲存储还是在”S3″,直接访问即可。读过的块可以放入缓冲。

写块

写块比较复杂。当发起一个写操作的时候,如果本地不存在或者正在同步,本地会先写入一个临时块,写入成功就返回成功。然后会找到”S3″上的块,下载合并修改。如果本地存在,并且该块不在同步中,就直接修改。

每隔一段时间,系统会建立一个check point,将修改的块增加一个版本号,同步到“S3”中去。这里的同步是异步的。全部完成算完成,不存在中间状态。如果操作系统对一个块修改10次,但这些修改操作在两次同步之间,只增加一个版本,避免重复。

缓冲存储损坏

如果缓冲存储损坏,如果没有Slave。由于S3和Mysql中有上一次的快照信息,所以可以恢复到最近的快照状态。不会出现一致性问题。但Check Point之间的时间间隔可能比较长,如果无法忍受的话,可以建立一个Slave用来记录所有的写操作,但缓冲存储损坏的时候,可以通过Slave上的数据恢复到最近的点。

总结

分布式的虚拟磁盘,可以通过两层存储架构,同时满足严格的一致性和分区要求,也有好的随机读写性能。之所以可以采取两层存储,是因为一块“盘”只能一台机器独享,不要求共享,相当于在可分区的特性上打了个折扣,所以这样应该是没问题的。

亚马逊的测试报告也是写的性能远远大于读性能,和这个架构的特点也是相似的。暂时没有发现什么冲突的地方。当然这个架构只是我的猜想,做不得数的。

好像除了亚马逊,没有其他公司对这种磁盘系统感兴趣,也许是没有必要吧。这样虚拟出来的磁盘性能有限,而且系统层次越多越不稳定。但“云计算”招摇撞骗,大行其道,探索一下也好。

文章分类 软件技术 | 标签: Amazon EBS, EBS, EBS架构 | 发表评论 |

Linux大文件传输

在 2012年2月7日 上公布 作者为 yankay
我们经常需要在机器之间传输文件。比如备份,复制数据等等。这个是很常见,也是很简单的。用scp或者rsync就能很好的完成任务。但是如果文件很大,需要占用一些传输时间的时候,怎样又快又好地完成任务就很重要了。在我的测试用例中,一个最佳的方案比最差的方案,性能提高了10倍。

复制文件

如果我们是复制一个未压缩的文件。这里走如下步骤:
  1. 压缩数据
  2. 发送到另外一台机器上
  3. 数据解压缩
  4. 校验正确性
这样做会很有效率,数据压缩后可以更有效的利用带宽

使用ZIP+SCP

我们可以通过ZIP+SCP的组合实现这个功能。
gzip -c /home/yankay/data | ssh yankay01 "gunzip -c - > /home/yankay/data"

这条命令是将/home/yankay/data经过GZIP压缩,通过ssh传输到yankay01的机器上。

data文件的大小是1.1GB,经过Zip压缩后是183MB,执行上面的命令需要45.6s。平均吞吐量为24.7MB/s
我们会发现Scp也有压缩功能,所以上面的语句可以写成
scp -C -c blowfish /home/yankay/data yankay01:/home/yankay/data

这样运行效果是相同的,不通之处在于我使用了blowfish算法作为Scp的密匙算法,使用这个算法可以比默认的情况快很多。单单对与scp,使用了blowfish 吞吐量是62MB/s,不使用只有46MB/s。

可是我执行上面一条命令的时候,发现还是需要45s。平均吞吐量还为24MB/s。没有丝毫的提升,可见瓶颈不在网络上。
那瓶颈在哪里呢?

性能分析

我们先定义几个变量

  • 压缩工具的压缩比是 CompressRadio
  • 压缩工具的压缩吞吐是CompressSpeed MB/s
  • 网络传输的吞吐是 NetSpeed MB/s

由于使用了管道,管道的性能取决于管道中最慢的部分的性能,所以整体的性能是:

$$speed=min(NetSpeed/CompressRadio,CompressSpeed)$$

当压缩吞吐较网络传输慢的时候,压缩是瓶颈;但网络较慢的时候,网络传输/吞吐 是瓶颈。

根据现有的测试数据(纯文本),可以得到表格:

压缩比 吞吐量 千兆网卡(100MB/s)吞吐量 千兆网卡吞吐量,基于ssh(62MB/s) 百兆网卡(10MB/s)吞吐量
ZLIB 35.80% 9.6 9.6 9.6 9.6
LZO 54.40% 101.7 101.7 101.7 18.38235294
LIBLZF 54.60% 134.3 134.3 113.5531136 18.31501832
QUICKLZ 54.90% 183.4 182.1493625 112.9326047 18.21493625
FASTLZ 56.20% 134.4 134.4 110.3202847 17.79359431
SNAPPY 59.80% 189 167.2240803 103.6789298 16.72240803
NONE 100% 300 100 62 10

可以看出来。在千兆网卡下,使用QuickLZ作为压缩算法,可以达到最高的性能。如果使用SSH作为数据传输通道,则远远没有达到网卡可以达到的最佳性能。在百兆网卡的情况下,各个算法相近。对比下来QuickLZ是有优势的。

对于不同的数据和不同的机器,可以得出不同的最佳压缩算法。但有一点是肯定的,尽量把瓶颈压在网络上。对于较慢的网络环境,高压缩比的算法会比较有优势;相反对于较快的网络环境,低压缩比的算法会更好。

结论

根据上面的分析结果,我们不能是用SSH作为网络传输通道,可以使用NC这个基本网络工具,提高性能。同时使用qpress作为压缩算法。

scp /usr/bin/qpress yankay01:/usr/bin/qpress
ssh yankay01 "nc -l 12345 |  qpress -dio > /home/yankay/data" &
qpress -o /home/yankay/data |nc yankay01 12345

第一行是将gpress安装到远程机器上,第二行在远程机器上使用nc监听一个端口,第三行压缩并传送数据。

执行上面的命令需要2.8s。平均吞吐量为402MB/s,比使用Gzip+Scp快了16倍!!

根据上文的公式,和自己的数据,可以绘出上面的表格,就可以选择出最适合的压缩算法和传输方式。达到满意的效果。如果是一个长期运行的脚本的话,这么做是值得的。

文章分类 软件技术 | 标签: Linux, 传输, 大文件 | 1 条评论 |

内存究竟有多快?

在 2012年2月6日 上公布 作者为 yankay

一般来说。CPU需要0个周期来访问其寄存器,1-30个周期来访问高速缓存,50-200个周期来访问主存。

对于Intel Core i7来说。这个值可以很具体。Intel Core i7的主频约在2-3GHz。可以计算出。

L1—指令缓存 L1-数据缓存 L2-缓存 L3-缓存 内存
访问周期 4 4 11 30-40 50-200
缓存大小 32KB 32KB 256KB 8MB 若干GB
访问时间 2ns 2ns 5ns 14-18ns 24-93ns

也就是说,访问内存的时间是ns级别的。

再来看看磁盘。

磁盘的访问时间=寻道时间+旋转延迟+数据传输时间。对于普通的7200转SATA磁盘。这个值是:9ms+4ms+0.02ms=13.02ms。

也就是说,如果从磁盘随机访问一个字节,需要13.02ms,比从内存获取的时间24-93ns,至少要多14万倍。相差5个数据级,何其巨大的差距。

顺序读写磁盘会快一些。 假设一个盘片有1000个扇区,每个扇区512字节,7200转。顺序读可以忽略掉寻道的时间。所以吞吐量是 扇区数×扇区大小×转速=1000*512/(60/7200)=58MB/s。这个数据似乎不咋样。如果使用多盘系统。SATA II的接口,吞吐量可以达到300MB/s。追求极限性能可以mount裸盘直接操作多盘。

存储器山

《深入理解计算机系统》一书中提到了一个存储器山的概念。教授先生别出心裁的将存储器的吞吐量,画成了一座山。

 

存储器山的测试程序是这样的:

Kernel_loop(elems, stride):
for (i = 0; i < elems; i += stride)
    result = data[i];

X轴表示的是读取步长,Y轴是吞吐量,Z轴是数据总量的大小。

可以看出来步长越小,数据数据总量越小。性能越好。

很明显,山是不是平滑的,是成阶梯状。红色部分为L1缓存,绿色为L2缓存,浅蓝是L3缓存,深蓝是内存。我们可以得一些数据。

L1-数据缓存 L2-缓存 L3-缓存 内存 磁盘 SSD
缓存大小 32KB 256KB 8MB 十几GB 几TB 几百GB
访问时间 2ns 5ns 14-18ns 24-93ns 13.0ms 30-300us
吞吐量 6500MB/s 3300MB/s 2200MB/s 800MB/s 60MB/s 250MB/s

也就是说,去除高速缓存的内存,吞吐量性能只有800MB/s而已。比起磁盘的300MB/s,网络的100MB/s。也只是快了几倍。平时说内存比磁盘快许多,其实没有那么多,如果不好好操作内存,内存的频繁读写,也可以成为系统瓶颈。

总结

现在处理器的主频已经停止了增长。但是高速缓存仍然以摩尔定律的速度增长的。长久的看,高速缓存频率逐渐会追上处理器的性能,容量也会越来越大。但是内存则不容乐观,虽然容量增加了许多,但是性能确没有大的提升,磁盘的状况也是类似;SSD刚刚开始普及,趋势不明显。

但可以看到,SSD的吞吐量和内存的吞吐量相去并不大。也就是说在未来,当SSD完全替代了磁盘。我们要像现在操作磁盘一样小心翼翼地操作内存,才有可能写出符合那个时代计算机性能的程序。相比之下,SSD的使用比磁盘要轻松一些,毕竟随机读写的速度在一两个数量级上。

文章分类 软件技术 | 标签: 内存, 性能 | 5 评论 |

并发编程之巧用锁

在 2012年1月8日 上公布 作者为 yankay

背景

程序都是跑在机器上的,并行CPU包含几个部分。

  • 处理器核心。执行线程的设备。一个核心可以执行一个或多个线程(超线程)
  • 互连线。处理器和处理器,处理器和储存器之间的通信通道。一般CPU分两种架构,SMP对称多处理器和NUMA架构。SMP的通信是总线式的,核心增加后性能会下降。NUMA的结果相当于以太网,性能相对较好。理论上NUMA是没有cache的,不过商业产品都有。互连线的资源是有限的。
  • 存储器。这里存储器值得是一级缓存,二级缓存和内存。一级缓存一般和处理器在一起,访问它需要1-2个时钟周期,访问二级缓存则需要数十个时钟周期。而访问内存,需要数百个时钟周期。
可以看出来,CPU访问内存的代价是很高的。如果一个处理器改变的一个volatile变量,为了保持一致性,需要将这个消息广播到其他的处理器,其他处理器废弃其一级缓存,重新加载。整个操作需要至少数十个时钟周期。而如果CPU仅仅访问其一级缓存的数据,性能就很高。所以多核编程和网络编程一样,性能的瓶颈就在于互连线的使用。
自转锁
自转锁是一种锁的实现方式。就是在while语句中不断监视一个变量,通过观察到变化,保证只有一个线程持有锁。
    public void lock(){
        while(state.getAndSet(true)){}
    }

这里的GetAndSet方法是一个CPU的CAS硬件指令。我们可以看到,每次调用这个质量,CPU要保证缓存一致,丢弃该CPU持有该变量的缓存,重新加载。会消耗至少数十个时钟周期的时间。所以可以进行如下改进。

    public void lock(){
        while(true){
            while(state.get()){};
            if(!state.getAndSet(true))
                return;
    }

这个锁和上面的锁逻辑上是一样的。区别是处理器在自旋的时候,不再执行CAS,而是执行一个get操作,直接从一级缓存中取数据,不主动同步。让发现状态有所变化后,再通过CAS操作确认并加锁。这样可以大大减少CAS指令的数量,节约了处理器之间的信息流量。

当然这个锁还有很多可以改进的地方,不详述了。不过其中“乐观”的思想是值得借鉴的。下面总结了一个用锁时候的方法,可以提高性能。

细粒度同步

细粒度同步避免对整个数据结构上锁。比如Java语言中的ConcurrentHashMap相对普通的HashMap性能要高很多。下图是ConcurrentHashMap的结构。

《Java并发编程之ConcurrentHashMap》很好的解释了ConcurrentHashMap的实现。

读写锁

许多共享对象都是这样的,读可以并发进行,不会去修改对象。而写操作需要线性进行。如果读操作远远大于写操作,区分读写行为,可以提高程序的并发性。

乐观同步

上面的自旋锁,就是一个乐观的例子。乐观就是先取出变量,乐观的认为没有冲突,在最后再确认下没有冲突,如果有冲突,重新再执行一边。这样就把麻烦的事情放到了最后,可以减少整段程序中加锁的部分,提高并行性。

惰性同步

惰性同步和乐观思路是一样的。在修改变量的时候,分两个阶段,先修改好,再在最后确认没有冲突,完成修改。

非阻塞操作

非阻塞操作就是CAS操作。使用这些操作可以避免使用加锁的传统方法,但实现一个非阻塞的共享数据结构非常的复杂,很容易出问题。性能的提升不是没有成本的。关于CAS操作,有一个缺陷,即ABA问题。CAS(compare and swap)操作的语义是(先比较原来的值和新的值是否一致,如果一致则更新并返回True,不一致则返回False)。这个操作不是原子的,中途如果有其他的线程先将这个地址修改为另一个值在改回来,一样会返回True。所以要注意这个特性,否则就可能造成一些Bug.

性能的提升很不容易,多点复杂性,就多点Bug的可能。并发编程的Bug还不怎么好找出来。所以我觉得,善用现有的久经考验的数据结构,少自己操作底层的原语,等到发现性能问题的时候,再在性能瓶颈的部分履薄冰地编程,尽可能少的引入复杂度。话说Java的LinkedTransferQueue使用了更精致的并发编程可以极大的提高队列的性能,可到Java7才成为JDK的一部分。可见实现一个并发的队列有多难了。

文章分类 软件技术 | 标签: 多核编程 | 发表评论 |

查询利器-bloom-filter详解

在 2011年11月13日 上公布 作者为 yankay

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。本文着重于在实现Bloom Filter的时候会使用到的一些技巧。

布隆过滤器的原理不难理解。相对于一个精简的HashMap的数据结构,存入数据的时候,不存入数据本身,只保存其Hash的值。可以用于判断该数据是否存在。其本质是用Hash对数据进行”有损压缩”的位图索引。详细参见。

Bloom_filter

 

错误率

如果用来存放Hash值的槽位足够多,那么碰撞的概率就会比较小。但是所占用的空间就会比较大。所以当分配空间的时候,需要通过你能容忍的错误率和需要存放的Key的数量来指定。如果所需存储的Key数量是n,错误率是p,所需要的槽位是m。有计算槽位的公式$$m=-frac{nln p}{(ln 2)^2}.$$,也有计算概率的公式$$p = left( 1-e^{-(m/nln 2) n/m} right)^{(m/nln 2)}$$。这些公式当然不是我推导出来的,想来也不太难,就不赘述推导过程了。下面这张图可以很好的表示n和m取不同的值的时候,p的值。

Bloom_filter

根据这张图。我们可以计算出所需要的内存使用量。如果把错误率控制在1%以下的话。

保存key数 占用空间
1万 64KB
10万 1MB
100万 16MB
1000万 256MB
1亿 <4GB

 

可见占用的空间在key的数量在百万级别还是很划算的,但到了上亿的级别就不那么划算了。

Bloom Filter的插入和查询都是常数级别的,所以最大的问题就是占用内存过大。而初次分配内存的时候,如果没有能够确认槽位的个数。如果分配过多会导致内存浪费,太少就会倒是错误率过高。下面提到的两个改进方案可以分别解决这两个问题。

折叠

折叠是指当你初始化一个Bloom Filter的时候,可以分配足够大的槽位,等到Key导入完毕后,可以对使用的槽位进行合并操作。具体方法是将槽位切成两半,一边完全叠加到另一边上。减少内存的使用量。检查key的代码要做稍许改变。例:

 

通过这个操作,可以使实际使用的内存量减半。多执行几次,能减少更多。

动态扩展

通过折叠操作,可以解决分配过大的问题,但是如果一开始分配过小,就需要扩展槽位才行。如何扩展呢?只要按原尺寸再建立一个Bloom Filter数组。原来的那个保存起来,不再写入。有新的写请求的时候,就将数据写入到新的那个Bloom Filter数组里面去。等到新的也写满了,就再建立一个,以此类推。查询的时候,就需要遍历每一个Bloom Filter数组才行。但因为查询一个Bloom Filter数组的速度很快,查询一组Bloom Filter数组也不会太影响性能。使用这种手段可以是Bloom Filter的大小可以轻易的扩展。但这样做有个的缺陷,就是错误率会随着数组的增加而上升,因为实际的数组长度并没有增加。

d-bloom-filter

通过上面的两个方法,就可以解决BloomFilter的分配内存的问题。但无论哪种方法都有自己局限性,折叠每次只能减半,不是很精确。动态增加的方法会造成错误率增加。最好还是能预先估计到这个BloomFilter的容量。

文章分类 软件技术 | 标签: Bloom Filter, Hadoop | 1 条评论 |

多核平台下的JAVA优化

在 2011年11月5日 上公布 作者为 yankay

现在多核CPU是主流。利用多核技术,可以有效发挥硬件的能力,提升吞吐量,对于Java程序,可以实现并发垃圾收集。但是Java利用多核技术也带来了一些问题,主要是多线程共享内存引起了。目前内存和CPU之间的带宽是一个主要瓶颈,每个核可以独享一部分高速缓存,可以提高性能。JVM是利用操作系统的”轻量级进程”实现线程,所以线程每操作一次共享内存,都无法在高速缓存中命中,是一次开销较大的系统调用。所以区别于普通的优化,针对多核平台,需要进行一些特殊的优化。

代码优化

线程数要大于等于核数

如果使用多线程,只有运行的线程数比核数大,才有可能榨干CPU资源,否则会有若干核闲置。要注意的是,如果线程数目太多,就会占用过多内存,导致性能不升反降。JVM的垃圾回收也是需要线程的,所以这里的线程数包含JVM自己的线程

尽量减少共享数据写操作

每个线程有自己的工作内存,在这个区域内,系统可以毫无顾忌的优化,如果去读共享内存区域,性能也不会下降。但是一旦线程想写共享内存(使用volatile关键字),就会插入很多内存屏障操作(Memory Barrier或者Memory Fence)指令,保证处理器不乱序执行。相比写本地线程自有的变量,性能下降很多。处理方法是尽量减少共享数据,这样也符合”数据耦合”的设计原则。

使用synchronize关键字

在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

使用乐观策略

传统的同步并发策略是悲观的。表现语义为:多线程操作一个对象的时候,总觉得会有两个线程在同时操作,所以需要锁起来。乐观策略是,假设平时就一个线程访问,当出现了冲突的时候,再重试。这样更高效一些。Java的AtomicInteger就是使用了这个策略。

使用线程本地变量(ThreadLocal)

使用ThreadLocal可以生成线程本地对象的副本,不会和其他线程共享。当该线程终止的时候,其本地变量可以全部回收。

类中Field的排序

可以将一个类会频繁访问到的几个field放在一起,这样他们就有更多的可能性被一起加入高速缓存。同时最好把他们放在头部。基本变量和引用变量不要交错排放。

批量处理数组

现在处理器可以用一条指令来处理一个数组中的多条记录,例如可以同时向一个byte数组中读或者写store记录。所以要尽量使用System.arraycopy()这样的批量接口,而不是自己操作数组。

JVM优化

启用大内存页

现在一个操作系统默认页是4K。如果你的heap是4GB,就意味着要执行1024*1024次分配操作。所以最好能把页调大。这个配额设计操作系统,单改Jvm是不行的。Linux上的配置有点复杂,不详述。

在Java1.6中UseLargePages是默认开启的,LasrgePageSzieInBytes被设置成了4M。笔者看到一些情况下配置成了128MB,在官方的性能测试中更是配置到256MB。

启用压缩指针

Java的64的性能比32慢,原因是因为其指针由32位扩展到64位,虽然寻址空间从4GB扩大到256 TB,但导致性能的下降,并占用了更多的内存。所以对指针进行压缩。压缩后的指针最多支持32GB内存,并且可以获得32位JVM的性能。

在JDK6 update 23默认开启了,之前的版本可以使用-XX:+UseCompressedOops来启动配置。

性能可以看这个评测,性能的提升是很可观。

benchmarka

启用NUMA

numa是一个CPU的特性。SMP架构下,CPU的核是对称,但是他们共享一条系统总线。所以CPU多了,总线就会成为瓶颈。在NUMA架构下,若干CPU组成一个组,组之间有点对点的通讯,相互独立。启动它可以提高性能。

NUMA需要硬件,操作系统,JVM同时启用,才能启用。Linux可以用numactl来配置numa,JVM通过-XX:+UseNUMA来启用。

激进优化特性

在Java1.6中,激进优化(AggressiveOpts)是默认开启的。激进优化是一般有一些下一个版本才会发布的优化选项。但是有可能造成不稳定。前段时间以讹传讹的JDK7的Bug,就是开启这个选项后测到的。

逃逸分析

让一个对象在一个方法内创建后,如果他传递出去,就可以称为方法逃逸;如果传递到别的线程,成为线程逃逸。如果能知道一个对象没有逃逸,就可以把它分配在栈而不是堆上,节约GC的时间。同时可以将这个对象拆散,直接使用其成员变量,有利于利用高速缓存。如果一个对象没有线程逃逸,就可以取消其中一切同步操作,很大的提高性能。

但是逃逸分析是很有难度的,因为花了cpu去对一个对象去分析,要是他不逃逸,就无法优化,之前的分析血本无归。所以不能使用复杂的算法,同时现在的JVM也没有实现栈上分配。所以开启之后,性能也可能下降。

可以使用-XX:+DoEscapeAnalysis来开启逃逸分析。

高吞吐量GC配置

对于高吞吐量,在年轻态可以使用Parallel Scavenge,年老态可以使用Parallel Old垃圾收集器。
使用-XX:+UseParallelOldGC开启

可以将-XX:ParallelGCThreads根据CPU的个数进行调整。可以是CPU数的1/2或者5/8

低延迟GC配置

对于低延迟的应用,在年轻态可以使用ParNew,年老态可以使用CMS垃圾收集器。

可以使用-XX:+UseConcMarkSweepGC和-XX:+UseParNewGC打开。

可以将-XX:ParallelGCThreads根据CPU的个数进行调整。可以是CPU数的1/2或者5/8

可以调整-XX:MaxTenuringThreshold(晋升年老代年龄)调高,默认是15.这样可以减少年老代GC的压力

可以-XX:TargetSurvivorRatio,调整Survivor的占用比率。默认50%.调高可以提供Survivor区的利用率

可以调整-XX:SurvivorRatio,调整Eden和Survivor的比重。默认是8。这个比重越小,Survivor越大,对象可以在年轻态呆更多时间。

等等

 

参见:《Java优化白皮书》,《深入理解Java虚拟机》

文章分类 软件技术 | 标签: Java, 优化, 多核 | 1 条评论 |

HBase中文官方文档

在 2011年11月1日 上公布 作者为 yankay

 

hbase

HBase – Hadoop Database,是一个构建在Apache Hadoop上的列数据。Hbase有很好的扩展性,被认为是BigTable的一个克隆,可以存储数以亿计的行。

在Hbase的官网,我们看到一篇很好的官方文档。我花了很长的时间,把他汉化了。请点击这里:《HBase中文官方文档》

这篇文档非常的详尽,从Hbase的安装,配置,使用,原理,优化,维护都做了非常全面的介绍。还提供了很多扩展阅读,方便我们知道更多Hbase的工作机制。通过翻译这篇文档,我细细读了这篇文档,受益良多。

翻译是一个吃进去再吐出来的过程,我的Hbase的理解也可能会发生一些偏差,所以如果有地方发生错误,请告诉我,大家一起进步。

 

文章分类 软件技术 | 标签: Hbase, 中文, 官方文档, 文档 | 发表评论 |

最小完美哈希函数简介

在 2011年10月16日 上公布 作者为 yankay

什么是保序最小完美哈希函数

我曾经花了很多脑筋来找一个很好很完美的哈希算法,但都没有想到,最近看到了,掩不住一阵激动分享下。最小完美哈希函数是什么,要从定义说起,这个名字很长,一步步解释。

  1. 哈希函数 任意函数h(x)都可以说哈希函数,一般来说,一个良好的哈希函数可以尽量避免重复。x的集合是参数域,h(x)的集合是值域。
  2. 完美哈希函数  完美哈希函数,就是完全不会冲突的哈希函数,这要求函数的值域至少比参数域要大
  3. 最小完美哈希函数 最小完美哈希函数,就是指函数的值域和参数域的大小完全相等,一个也不多
  4. 保序最小完美哈希函数 保序的意思就是指这个哈希之后顺序是不变的,同时还能满足其他两个条件。
这个函数的优点就是形式上很完美,就像给一个排好序的文档编上的序号一般紧凑可靠。但是这个函数有两个缺点,一是必须事前必须知道原数据集,二是需要花一定的CPU来生成这个函数。我认为,对于数据仓库类的线下搜索应用,这个算法是有用武之地的。但对于强调实时的数据业务,这个算法是不适合的。

算法实现

该算法的实现方法是这样的。先构造两个普通的哈希函数h1(x)和h2(x),还有一个用数组实现的函数g(x)。使得$$h(x)=g(h1(x))+g(h2(x)) mod n$$,其中n是参数的总个数,H(x)就是最终的有序最小完美哈希函数了。

以上是定义,说不清楚,举个例子就明白了。取一个n为12的数据集。

首先构造这三个函数:
函数h1和h2:

x h1函数 h2函数
jezebel 5 9
jezer 5 7
… … …

函数g:

x g函数
5 0
7 1
9 0
… …
根据上文的公式$$h(x)=g(h1(x))+g(h2(x)) mod n$$,可以得出:
x h计算步骤 h值
jezebel g(5)+g(9) 0
jezer g(5)+g(7) 1
… … …

这里的h就可以最小完美哈希函数算出的值了,很神奇,不是吗?

大致的流程走了一遍,现在最最关键的是h1(x),h2(x)和g(x)是怎么得来的。

h1(x)和h2(x)比较简单,可以使用一个很简便的方法获得:先定义一个权重数组w[i],这个数据是一系列随机的数。$$h1=(t[1]*w[1]+t[2]*w[2]+…+t[i]*w[i]) mod m$$。其中t[i]指得的字符串x的第i个字符,m值得的函数的值域。只要更换一个权重数组,就可以重新构造一个新函数。有很多方法可以构造这两个函数。

g(x)的获得就比较复杂。可以是凑出来的。就如同上面的例子,因为$$g(5)+g(9) mod 12=0$$,$$g(5)+g(7) mod 12=1$$。所以我们可以凑出$$g(5)=0,g(7)=1,g(9)=0$$这样就可以满足上面的两个条件了。需要一个数组来存储函数g的结果,当然凑也不能瞎凑,是有方法的,下面专门讲凑的步骤。

算法函数生成

首先随意设定一个数,比如是7,设为$$g(7)=1$$,因为我们已知$$g(5)+g(7) mod 12=1$$,所以可以推论出$$g(5)=0$$。因为$$g(5)+g(9) mod 12=0$$,所以可以推出$$g(9)=0$$,以此类推就可以了。但要注意的是千万不能重复设定一个数两次,这样就会形成一个环,永远也推不完。所以遇到已经推算过的数的时候,要检测环的存在。这样下去,就可以猜出全部的值了。

现在需要的就是分析这个凑的过程的运行效率问题。这个就要涉及到h1,h2这两个函数的值域大小,如果越大,越容易凑出一个g函数,但是g函数的参数域就会比较大,存储这个g函数的数据就需要占用更多的空间。相反如果值域越小,在凑的时候就非常容易出现环,需要更长的时间才能凑出这个g函数。

怎么办呢?

我们可以使用3个的h函数来降低形成环的可能,就是这样$$h(x)=g(h1(x))+g(h2(x))+g(h3(x)) mod n$$,这样虽然推理g函数的过程会复杂一些,但是很有效,有实验分析表明,当h函数的值域大约参数域的1.23倍的时候,这个g函数的创建尝试次数是常数。

至此,这个算法介绍完了。这个方法是从《Managing Gigabytes》这本书看到的,这里的讲述更浅显一些。

结语

这个哈希函数是一个静态Hash函数,可以非常有效的缩减索引所需要的空间。《Managing Gigabytes》一书中有一个对比,如果直接使用字符串数组,100万个术语需要28MB的空间,而是要这样的哈希函数,可以缩减到12MB。要知道索引小一点,磁盘就能读得快一点,查询就能快一点。所以这个哈希函数对于提高性能是非常给力的。

但是他是静态的,就意味着事前必须知道需要哈希哪些数据。同时生成的算法比较复杂,需要很长的时间来建立索引。没有办法实时添加更新。给他的应用范围提了个极大的限制。窃以为输入法的词库,数据仓库的查询索引,还有一些不需要更新且对性能有要求的场景,这个算法是适用的。

文章分类 软件技术 | 标签: opmphf, 哈希, 最小完美哈希函数 | 1 条评论 |
« 上一页
下一页 »

近期文章

  • 听说 Docker 被 kubenetes 抛弃了,怎么办?containerd
  • 公告 – 博客重开了
  • CloudFoundry v2面面谈,内赠MicroCFv2福利
  • Docker能够运行任何应用的“PaaS”云
  • Scala Tour – 精选

近期评论

  • [整理]完美哈希函数(Perfect Hash Function) - 高性能架构探索发表在《最小完美哈希函数简介》
  • Scala Tour – 精选 - Java天堂发表在《Scala Tour – 精选》
  • Golang适合高并发场景的原因分析 - 站壳网发表在《Go-简洁的并发》
  • HBase 官方文档 – 源码巴士发表在《Windows下eclipse的 C++环境配置》
  • Go-简洁的并发-点开发表在《Go-简洁的并发》

归档

  • 2021年6月
  • 2021年3月
  • 2014年2月
  • 2013年9月
  • 2013年5月
  • 2013年1月
  • 2012年11月
  • 2012年9月
  • 2012年8月
  • 2012年3月
  • 2012年2月
  • 2012年1月
  • 2011年11月
  • 2011年10月
  • 2011年9月
  • 2010年10月
  • 2010年8月
  • 2010年7月
  • 2010年6月
  • 2010年5月
  • 2010年4月
  • 2010年3月
  • 2010年2月
  • 2010年1月
  • 2009年10月
  • 2009年9月
  • 2009年8月
  • 2009年7月
  • 2009年6月
  • 2008年10月
  • 2008年8月
  • 2008年7月
  • 2008年6月

分类目录

  • 家庭生活
  • 未分类
  • 每日心得
  • 软件技术

友情链接

  • DaoCloud Enterprise
  • DaoCloud 云原生一体机

CyberChimps WordPress Themes

沪ICP备2021008917号-1 © 颜开