为何Java中clone与System.arraycopy比手动循环拷贝数组开销更高?
骑士拨号器数组拷贝性能问题分析
测试背景
我最近在LeetCode上求解骑士拨号器问题,两次提交的代码仅数组拷贝方式不同:
第一次提交(System.arraycopy实现)
提交链接,代码如下:
class Solution { public int knightDialer(int n) { int[] prev_dp = new int[10]; int[] curr_dp = new int[10]; int mod = 1000000007; List<List<Integer>> adjList = new ArrayList<>(); adjList.add(new ArrayList<>(Arrays.asList(4,6))); adjList.add(new ArrayList<>(Arrays.asList(6,8))); adjList.add(new ArrayList<>(Arrays.asList(7,9))); adjList.add(new ArrayList<>(Arrays.asList(4,8))); adjList.add(new ArrayList<>(Arrays.asList(0,3,9))); adjList.add(new ArrayList<>(Arrays.asList())); adjList.add(new ArrayList<>(Arrays.asList(0,1,7))); adjList.add(new ArrayList<>(Arrays.asList(2,6))); adjList.add(new ArrayList<>(Arrays.asList(1,3))); adjList.add(new ArrayList<>(Arrays.asList(2,4))); if(n==0) return 0; // for 1st step; for(int i = 0; i<10;i++){ curr_dp[i] = 1; } // prev_dp = curr_dp.clone(); System.arraycopy(curr_dp,0, prev_dp,0,10); for(int j = 2; j<=n;j++){ for(int i = 0; i<10;i++){ int curr_sum = 0; for(int adjNode: adjList.get(i)){ curr_sum+=prev_dp[adjNode]; curr_sum = curr_sum%mod; } curr_dp[i] = curr_sum; } // prev_dp = curr_dp.clone(); System.arraycopy(curr_dp,0, prev_dp,0,10); } int ans = 0; for(int i = 0; i<10;i++){ ans+=curr_dp[i]; ans = ans%mod; } return ans; } }
第二次提交(手动for循环实现)
提交链接,代码如下:
class Solution { public int knightDialer(int n) { int[] prev_dp = new int[10]; int[] curr_dp = new int[10]; int mod = 1000000007; List<List<Integer>> adjList = new ArrayList<>(); adjList.add(new ArrayList<>(Arrays.asList(4,6))); adjList.add(new ArrayList<>(Arrays.asList(6,8))); adjList.add(new ArrayList<>(Arrays.asList(7,9))); adjList.add(new ArrayList<>(Arrays.asList(4,8))); adjList.add(new ArrayList<>(Arrays.asList(0,3,9))); adjList.add(new ArrayList<>(Arrays.asList())); adjList.add(new ArrayList<>(Arrays.asList(0,1,7))); adjList.add(new ArrayList<>(Arrays.asList(2,6))); adjList.add(new ArrayList<>(Arrays.asList(1,3))); adjList.add(new ArrayList<>(Arrays.asList(2,4))); if(n==0) return 0; // for 1st step; for(int i = 0; i<10;i++){ curr_dp[i] = 1; } // prev_dp = curr_dp.clone(); // System.arraycopy(curr_dp,0, prev_dp,0,10); for(int i = 0; i<10;i++){ prev_dp[i] = curr_dp[i]; } for(int j = 2; j<=n;j++){ for(int i = 0; i<10;i++){ int curr_sum = 0; for(int adjNode: adjList.get(i)){ curr_sum+=prev_dp[adjNode]; curr_sum = curr_sum%mod; } curr_dp[i] = curr_sum; } // prev_dp = curr_dp.clone(); // System.arraycopy(curr_dp,0, prev_dp,0,10); for(int l = 0; l<10;l++){ prev_dp[l] = curr_dp[l]; } } int ans = 0; for(int i = 0; i<10;i++){ ans+=curr_dp[i]; ans = ans%mod; } return ans; } }
测试结果
第一次提交运行速度约为第二次的3倍(第一次耗时70ms,第二次耗时203ms),内存消耗仅为第二次的1/4左右,测试截图如下:
问题解答
首先需要注意:你描述的疑问和实际测试结果刚好相反,实际测试中System.arraycopy的性能远优于手动循环拷贝,原理如下:
1. 运行速度差异原因
System.arraycopy是JVM实现的native本地方法,底层直接操作内存块进行批量复制,不需要像手动for循环一样逐次执行Java层的数组边界检查、索引寻址、循环计数等指令,还能直接利用CPU的批量读写指令优化,即使是10个元素的小数组,执行效率也远高于手写循环。- 手动for循环的10次赋值操作,在Java层要经历10次数组越界检查、10次内存写入操作,即使JIT即时编译做了优化,也很难达到native层内存批量拷贝的效率,因此耗时更高。
- 如果是使用
clone()方法拷贝数组,会额外创建新的数组对象,带来对象创建和GC的开销,这种场景下才可能比手动循环速度更慢,但你本次测试并没有启用clone的逻辑。
2. 内存消耗差异原因
System.arraycopy直接在已经创建好的prev_dp数组内存地址上写入数据,没有额外的对象创建开销,调用native方法的栈内存开销也远低于Java层循环的开销。- 手动循环执行过程中会产生循环计数变量、操作数栈临时数据等额外的栈内存开销,加上LeetCode OJ平台的内存统计本身存在一定浮动误差,多次运行统计的平均内存就会比
System.arraycopy实现更高。
内容的提问来源于stack exchange,提问作者Aditya Maheshwari
相关产品推荐
相关产品推荐

