【问题标题】:Android: why is native code so much faster than Java codeAndroid:为什么原生代码比 Java 代码快这么多
【发布时间】:2014-02-19 09:03:39
【问题描述】:

在以下 SO 问题中:https://stackoverflow.com/questions/2067955/fast-bitmap-blur-for-android-sdk@zeh 声称将 Java 模糊算法移植到 C 的速度快 40 倍。

鉴于大部分代码仅包含计算,并且所有分配仅在实际算法数字运算之前“一次”完成 - 谁能解释为什么此代码运行速度快 40 倍? Dalvik JIT 不应该翻译字节码并显着降低与本机编译代码速度的差距吗?

注意:我自己还没有确认这个算法的 x40 性能提升,但我遇到的所有严重的 Android 图像处理算法都使用 NDK - 所以这支持了 NDK 代码运行得更快的概念。

【问题讨论】:

  • 有很多地方可以失去性能。如果您对这些实现特别感兴趣,请对它们进行概要分析并查看花费的时间。

标签: android performance android-ndk jit dalvik


【解决方案1】:

对于对数据数组进行操作的算法,有两件事会显着改变 Java 和 C 等语言之间的性能:

  • 数组边界检查 - Java 将检查每个访问 bmap[i],并确认 i 在数组边界内。如果代码试图越界访问,您将得到一个有用的异常。 C & C++ 不检查任何东西,只相信你的代码。对越界访问的最佳响应是页面错误。更可能的结果是“意外行为”。

  • 指针 - 使用指针可以显着减少操作。

以这个普通滤镜(类似于模糊,但 1D)为例:

for(i=0; i<ndata-ncoef; ++i) {
  z[i] = 0;
  for(k=0; k<ncoef; ++k) {
    z[i] += c[k] * d[i+k];
  }
}

当你访问一个数组元素时,coef[k] 是:

  • 将数组coef的地址加载到寄存器中
  • 将值 k 加载到寄存器中
  • 求和
  • 去那个地址获取内存

这些数组访问中的每一个都可以改进,因为您知道索引是顺序的。编译器,也不是 JIT,可以知道索引是顺序的,所以不能完全优化(尽管他们一直在尝试)。

在 C++ 中,您会编写更像这样的代码:

int d[10000];
int z[10000];
int coef[10];
int* zptr;
int* dptr;
int* cptr;
dptr = &(d[0]); // Just being overly explicit here, more likely you would dptr = d;
zptr = &(z[0]); // or zptr = z;
for(i=0; i<(ndata-ncoef); ++i) {
  *zptr = 0; 
  *cptr = coef;
  *dptr = d + i;
  for(k=0; k<ncoef; ++k) {
    *zptr += *cptr * *dptr;
    cptr++;
    dptr++;
  }
  zptr++;
}
       

当你第一次做这样的事情(并成功地让它正确)时,你会惊讶于它的速度有多快。所有取索引和索引和基地址求和的数组地址计算都被增量指令代替。

对于 2D 数组操作(例如图像模糊),无害代码 data[r,c] 涉及两个值获取,一个乘法和一个求和。因此,使用 2D 数组,指针的好处可以让您删除乘法运算。

因此,该语言可以真正减少 CPU 必须执行的操作。代价是 C++ 代码难以阅读和调试。指针错误和缓冲区溢出是黑客的食物。但是对于原始数字磨削算法,速度提升太诱人了,不容忽视。

【讨论】:

  • 也很重要:Dalvik JIT 执行的优化在一定程度上受到限制(参见stackoverflow.com/questions/4912695/…)。请注意,数组边界检查可能不是一个重要的性能问题——如果编译器可以确定可能的索引值的范围必须在 0 和 N 之间,它可以生成在数组大小和数组大小之间进行单个运行时比较的代码进入循环之前的 N。
  • @jdr5ca “for{..”是for循环错字吗?还是这样做是为了避免在此处发布答案时输入错误?或者实际上有允许它的c ++语法?我糊涂了。您在此答案中始终以这种方式输入。
  • 好的,花括号错了,6 年后改正了。
【解决方案2】:

上面没有提到的另一个因素是垃圾收集器。问题是垃圾收集需要时间,而且它可以随时运行。这意味着创建大量临时对象的 Java 程序(请注意,某些类型的 String 操作可能对此不利)经常会触发垃圾收集器,这反过来会降低程序(应用程序)的速度。

【讨论】:

  • 还有很多相关的开销。例如android 编译器将Suspend Check(检查 GC)指令放入循环中,因此应用程序不会“饿死”堆。仅此一项,即使是在原始一维数组(其中的东西非常接近 C 原生版本)上包含一些数学运算的循环中,也会导致显着减速。
【解决方案3】:

以下是基于级别的编程语言列表,

  • 汇编语言(机器语言,情人级)
  • C语言(中级)
  • C++、Java、.net、(高级)

这里低级语言可以直接访问硬件。只要级别增加,对硬件的访问就会减少。所以汇编语言的代码以最高的速度运行,而其他语言的代码根据它们的级别运行。

这就是 C 语言的代码运行速度比 Java 的代码快得多的原因。

【讨论】:

  • 这是错误的。我绝对不会将 C++ 置于与 Java 或 C# 相同的级别。当然 C++ 提供了大量的范例,但在比较生成的二进制文件时,它更接近于 C 而不是其他一切。尤其是在管理 Java 和 .NET 时。此外,性能不仅仅是一种语言“与金属有多接近”的问题。如果你的算法很差,无论你选择什么语言都会很慢。
  • 语言水平不是原因。编译与解释是这里的主要性能驱动因素,假设算法及其内存访问模式(缓存!)大致相同。
  • C++ 和 C 生成汇编代码,而 Java、c# 生成由中间解释器执行的代码。这就是主要区别所在。上面的列表是不正确的。 C/C++ 处于同一“级别”。仍然没有回答这个问题:在具有 JIT 计算的现代 VM 上,即使中间有垃圾收集运行,运行速度也应该比编译代码慢 10-30%,有时甚至不会。当然不会慢 4 倍。
猜你喜欢
  • 2015-03-26
  • 2011-08-22
  • 1970-01-01
  • 1970-01-01
  • 2023-03-05
  • 2014-11-10
  • 1970-01-01
  • 2011-05-27
  • 2011-05-28
相关资源
最近更新 更多