Android:为什么本地代码比Java代码快得多?

14
在下面的SO问题中:https://dev59.com/5nI95IYBdhLWcg3w-DH0,@zeh声称将一个java模糊算法移植到C后运行速度提高了40倍。
鉴于大部分代码仅涉及计算,并且所有分配仅在实际算法处理之前“一次性”完成——有人能解释为什么这段代码运行速度会提高40倍吗? Dalvik JIT难道不应该将字节码转换并大幅缩小到本地编译代码的速度差异吗?
注意:我个人尚未确认此算法的40倍性能增益,但我遇到的所有严肃的Android图像操作算法都使用NDK——因此这支持NDK代码将运行得更快的观点。

有很多地方可能会导致性能下降。如果你对这些具体的实现感兴趣,可以对它们进行分析,并查看时间花费在哪里。 - laalto
3个回答

21

对于操作数据数组的算法,在Java和C等语言中,有两个因素会显著影响性能:

  • 数组边界检查:Java会检查每次访问(例如bmap[i])并确认i是否在数组边界内,如果代码试图访问超出边界,则会抛出有用的异常。C和C ++不检查任何内容,只是信任你的代码。对于越界访问,最好的情况是页面错误,更有可能的结果是“意外行为”。

  • 指针:使用指针可以显著减少操作。

以此无害示例为例,展示一个常见的滤镜(类似于模糊,但1D):

for(int i = 0; i < ndata - ncoef; ++i) {  
    z[i] = 0;  
    for(int 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(int i = 0; i < (ndata - ncoef); ++i) {  
    *zptr = 0; 
    *cptr = coef;  
    *dptr = d + i;  
    for(int k = 0; k < ncoef; ++k) {  
        *zptr += *cptr * *dptr;  
        cptr++;  
        dptr++;  
    }  
    zptr++;  
}  
       

第一次尝试这样做时(并成功地得到正确的结果),你将惊讶于它可以更快地完成。获取索引,求和索引和基地址的所有数组地址计算都被替换为增量指令。

对于2D数组操作,例如对图像进行模糊处理,一个简单的代码data[r,c]涉及两个值的获取、一个乘法和一个求和。因此,使用指针可以消除乘法运算。

因此,该语言可真正减少CPU必须执行的操作。代价在于C++代码难以阅读和调试。指针错误和缓冲区溢出是黑客的温床。但当涉及到原始数字计算算法时,其速度提高太诱人了,不能忽视。


4
同样非常重要的是:Dalvik JIT 所执行的优化在某种程度上有一定的限制(参见https://dev59.com/0G445IYBdhLWcg3wWI2L)。请注意,数组边界检查可能不是一个重要的性能问题--如果编译器可以确定可能的索引值范围必须在0和N之间,它可以生成代码,在进入循环之前对数组大小和N进行单次运行时比较。 - fadden
@jdr5ca 这个 "for{..}" 是一个 for 循环的打字错误吗?还是为了避免在这里发布答案时出现打字错误而这样做的?或者实际上有一种 C++ 语法可以允许这样做?我感到困惑。你在这个答案中一直以这种方式输入它。 - marcius.tan
3
好的,花括号的语法错误已经在六年后修正了。 - jdr5ca

0

上面没有提到的另一个因素是垃圾回收器。问题在于垃圾回收需要时间,而且可以在任何时候运行。这意味着创建大量临时对象的Java程序(请注意,某些类型的字符串操作可能会对此产生不良影响)通常会触发垃圾回收器,从而减慢程序(应用程序)的速度。


还有许多相关的开销。例如,Android编译器在循环中放置了一个“暂停检查”(检查GC),以便应用程序不会因为堆而“饥饿”。即使在对原始1D数组进行一些数学运算的循环中(其中事情非常接近C本地版本),仅此就可能导致显着的减速。 - Paschalis

-9
以下是基于级别的编程语言列表,
- 汇编语言(机器语言,低级别) - C语言(中级) - C++、Java、.net(高级)
低级别语言可以直接访问硬件。随着级别的提高,对硬件的访问减少。因此,汇编语言的代码运行速度最快,而其他语言的代码根据它们的级别运行。
这就是为什么C语言的代码比Java的代码运行得更快的原因。

3
这是错误的。我绝不会把C++置于Java或C#相同的水平上。确实,C++提供了很多编程范式,但与其他语言相比,它更接近C语言,而不是其他的。特别是当与Java和.NET这些受管理的语言进行比较时,由于性能问题,二进制结果差别更大。此外,性能不仅取决于语言的“接近底层程度”,如果你的算法质量差,无论你选择哪个语言,都会很慢。 - ereOn
2
语言级别不是原因。编译与解释是主要的性能驱动因素,假设算法及其内存访问模式(缓存!)大致相同。 - laalto
C++和C生成汇编代码,而Java、C#生成由中间解释器执行的代码。这就是主要区别所在。上述列表不正确。C/C++处于同一“级别”。 这仍然没有回答问题:在现代VM中,即使在垃圾收集运行期间,使用JIT计算的速度应该比编译代码慢10-30%%,有时甚至不到这个速度。肯定不会慢4倍。 - Guy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接