起因
我的一个项目使用 Kotlin 编写,他是一个多维数据库应用程序,所以会非常频繁的操作 int 数组,其中有段程序就需要进行 几亿次的数组清除动作,类似这样的代码:
Arrays.fill(target, 0);
这个Arrays.fill其实就是jdk自带的一个实现,非常简陋,就是一个for循环填充数据。
所以我想改进他,将常见的数组长度编写成单个的实现,比如清除8个长度的方法如下:
fun clear8(target: IntArray) { if(target.size < 8){ throw IndexOutOfBoundsException() } target[0] = 0 target[1] = 0 target[2] = 0 target[3] = 0 target[4] = 0 target[5] = 0 target[6] = 0 target[7] = 0 }
不要怀疑你的眼睛,这样的写法通常是有效的。好的编译器会优化我写的代码,当然,更好的编译器会优化一个简单数组的for循环,这是后话。
那我们就测试一下吧。
import java.util.* import kotlin.system.measureNanoTime fun main() { test3() } private fun test3() { val size = 8 val time2 = measureNanoTime { val target = IntArray(size) for (i in 0 until 10_0000_0000) { IntArrays.clear8(target) } } println("fill$size $time2") val time1 = measureNanoTime { val target = IntArray(size) for (i in 0 until 10_0000_0000) { Arrays.fill(target, 0) } } println("Arrays.fill$size $time1") println() } internal object IntArrays { fun clear8(target: IntArray) { if(target.size < 8){ throw IndexOutOfBoundsException() } target[0] = 0 target[1] = 0 target[2] = 0 target[3] = 0 target[4] = 0 target[5] = 0 target[6] = 0 target[7] = 0 } }
测试结果:
fill8 55,408,200
Arrays.fill8 2,262,171,100
可以看出,使用展开的方式,比java自带的2.2秒,性能提高了40倍!!
与Java的性能对比
我感叹kotlin的编译器真的很强,但仔细一想,不对啊, Kotlin 就是基于 JVM 的,功劳应该是 java 的虚拟机运行时很厉害,所以如果这个程序如果转化为java直接编写是不是更快,至少性能一致吧。说干就干。
//IntArrays.java import java.util.Arrays; final class IntArrays { static void clear8(int[] target) { /* if (target.length < 8){ throw new IndexOutOfBoundsException(); }*/ target[0] = 0; target[1] = 0; target[2] = 0; target[3] = 0; target[4] = 0; target[5] = 0; target[6] = 0; target[7] = 0; } } // IntArraysDemoJava.java import java.util.Arrays; public final class IntArraysDemoJava { public static void main(String[] var0) { test1(); } private static void test1() { long count = 1000000000; long start = System.nanoTime(); final int[] target = new int[8]; for(int i = 0; i < count; i++) { IntArrays.clear8(target); } long time2 = System.nanoTime() - start; System.out.println("fill8 " + time2); start = System.nanoTime(); for(int i = 0; i < count; i++) { Arrays.fill(target, 0); } long time1 = System.nanoTime() - start; System.out.println("Arrays.fill8 " + time1); System.out.println(); } }
测试结果如下:
fill8 2,018,500,800
Arrays.fill8 2,234,306,500
天啊,在java下这种优化几乎没有效果,java我没有找到什么 release编译参数的概念,最多只有debug = false,我是在gradle中包含
compileJava { options.debug = false }
那么就是说,Kotlin生成的字节码要好于 Java生成的字节码?
Java Kotlin ALOAD 0 ALOAD 1 ICONST_0 ICONST_0 ICONST_0 ICONST_0 IASTORE ASTORE ALOAD 0 ALOAD 1 ICONST_1 ICONST_1 ICONST_0 ICONST_0 IASTORE IASTORE
字节码稍微不同,你要是问我为什么? 我母鸡啊。。。。。。
与C# 的对比
作为一个 .net 的死忠粉,这个时候就会想着是不是 c# 更快一些,更何况 .net core 3做了大量的性能优化,
class Program { static void Main(string[] args) { Test3.test1(); } } class Test3 { public static void test1() { long count = 1000000000; var watch = System.Diagnostics.Stopwatch.StartNew(); int[] target = new int[8]; for (int i = 0; i < count; i++) { Clear8(target); } watch.Stop(); Console.WriteLine("fill8 " + watch.Elapsed); watch.Restart(); for (int i = 0; i < count; i++) { Array.Clear(target, 0,8); } watch.Stop(); Console.WriteLine("Array.Clear8 " + watch.Elapsed); Console.WriteLine(); } static void Clear8(int[] target) { /* if (target.Length < 8) { throw new IndexOutOfRangeException(); }*/ target[0] = 0; target[1] = 0; target[2] = 0; target[3] = 0; target[4] = 0; target[5] = 0; target[6] = 0; target[7] = 0; } }
测试成绩:
fill8 00:00:02.7462676
Array.Clear8 00:00:08.4920514
和Java比起来还要慢,甚至系统自带的Array.clear更加慢,这怎么能让我忍,于是一通的 Span.Fill(0),结果更不理想。
和Nim对比的性能
兴趣提起来了,那就使用C语言实现一个....... 没写出来,我笨......,那就使用 Rust 实现一个,还是没有实现出来,按照教程一步步写,还是没有搞定..........
最后折腾出来一个 Nim 环境,嗯,还是这个简单。
import times, strutils proc clear8*[int](target: var seq[int]) = target[0] = 0 target[1] = 0 target[2] = 0 target[3] = 0 target[4] = 0 target[5] = 0 target[6] = 0 target[7] = 0 proc clear*[int](target: var seq[int]) = for i in 0..<target.len: target[i] = 0 proc test3() = const size = 8 var start = epochTime() var target = newseq[int](size) for i in 0..<10_0000_0000: target.clear8() let elapsedStr = (epochTime() - start).formatFloat(format = ffDecimal, precision = 3) echo "fill8 ", elapsedStr start = epochTime() for i in 0..<10_0000_0000: target.clear() let elapsedStr2 = (epochTime() - start).formatFloat(format = ffDecimal, precision = 3) echo "Arrays.fill ", elapsedStr2 test3()
测试成绩,注意要加 --release 参数。
fill8 3.499
Arrays.fill 5.825
失望,及其失望。
备注
所有测试是在我的台式机上进行的,配置如下:
AMD Ryzen 5 3600 6 Core 3.59 Ghz
8 GB RAM
Windows 10 64 专业版
所有测试都使用release编译。