现代的OO语言可以与C ++的数组存储性能竞争吗?


40

我只是注意到,我至少有点熟悉的每种现代OO编程语言(基本上只是Java,C#和D)都允许协变量数组。也就是说,字符串数组是对象数组:

Object[] arr = new String[2];   // Java, C# and D allow this

协变量数组是静态类型系统中的一个漏洞。它们使得可能在编译时无法检测到的类型错误,因此必须在运行时检查对数组的每次写入:

arr[0] = "hello";        // ok
arr[1] = new Object();   // ArrayStoreException

如果我进行大量阵列存储,这似乎会降低性能。

C ++没有协变数组,因此不需要进行此类运行时检查,这意味着不会降低性能。

是否进行了任何分析以减少必要的运行时检查次数?例如,如果我说:

arr[1] = arr[0];

有人会说这家商店不可能倒闭。我敢肯定,我还没有想到其他很多可能的优化方法。

现代编译器实际上是在进行这类优化吗?还是我不得不忍受这样一个事实,例如Quicksort总是执行O(n log n)不必要的运行时检查?

现代的OO语言是否可以避免因支持协变量数组而产生的开销?


7
我很困惑,为什么您建议C ++甚至在C ++不支持的功能上比其他语言快。

17
@eco:C ++可以更快地访问数组,因为它不支持协变量数组。弗雷德想知道“现代OO语言”是否有可能消除协变量数组的开销以接近C ++速度。
Mooing Duck 2012年

13
“可编译但在运行时引发异常的代码是个坏消息”

4
@Jesse数以百万计的人用动态语言编写可靠的,高度可扩展的代码。Imo:如果您不为代码编写测试用例,那么我不在乎是否存在编译器错误,无论如何我都不会信任它。
Voo 2012年

7
@Jesse还有什么时候会在运行时遇到异常?问题不在于在运行时引发异常的代码-在很多情况下这很有道理-问题在于保证代码是错误的,不会被编译器静态捕获,而是导致异常运行。
乔纳森·M·戴维斯

Answers:


33

D 没有协变数组。它允许他们在最新版本(dmd 2.057)之前使用,但该错误已修复。

D中的数组实际上只是具有指针和长度的结构:

struct A(T)
{
    T* ptr;
    size_t length;
}

索引数组时通常会进行边界检查,但使用时会删除边界检查-release。因此,在发布模式下,C / C ++中的数组与D中的数组之间没有真正的性能差异。


似乎不出现-d-programming-language.org/arrays.html说:“静态数组T[dim]可以隐式转换为以下之一:... U[]...动态数组T[]可以隐式转换为以下之一:U[]。 .. U的基类在哪里T?”
Ben Voigt 2012年

2
@BenVoigt然后需要更新在线文档。不幸的是,它们并非总是100%都是最新的。转换将与工作const U[],从此你不能分配错误类型的数组中的元素,但T[]绝对不会转换为U[]只要U[]是可变的。像以前那样允许协变数组是一个严重的设计缺陷,现已纠正。
乔纳森·M·戴维斯

@JonathanMDavis:协变量数组很棘手,但是对于特定的代码场景来说效果很好,该代码只会写入从同一数组读取的数组元素。D如何允许人们编写一种可以对任意类型的数组进行排序的方法?
2013年

@supercat如果要编写对任意类型进行排序的函数,请对其进行模板化。例如dlang.org/phobos/std_algorithm.html#sort
Jonathan M Davis,

21

是的,一个关键的优化是这样的:

sealed class Foo
{
}

在C#中,此类不能是任何类型的超类型,因此可以避免检查type数组Foo

第二个问题是,在F#协变量数组中是不允许的(但是我想除非在运行时优化中发现不必要,否则检查将保留在CLR中)

let a = [| "st" |]
let b : System.Object[] = a // Fails

https://stackoverflow.com/questions/7339013/array-covariance-in-f

一个有点相关的问题是数组边界检查。关于CLR中进行的优化,这可能是一个有趣的(但很旧的)读物(协方差也被提到1位):http : //blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds检查消除在clr.aspx


2
Scala还阻止了这种构造:val a = Array("st"); val b: Array[Any] = a是非法的。(但是,由于使用了底层的JVM,Scala中的数组是……特殊的魔术……)
pst 2012年

13

Java答案:

我认为您实际上尚未对代码进行基准测试,对吗?通常,Java中所有动态转换的90%是免费的,因为JIT可以消除它们(快速排序是一个很好的例子),其余的是一个ld/cmp/br绝对可预测的序列(如果不是,那么为什么这是您的代码抛出的原因)所有这些动态强制转换例外?)。

我们比实际比较早做负载,在所有情况下都正确预测了该分支的99.9999%(组成统计量!),因此我们不会使管道停顿(假设我们不会用负载打入内存,如果不太明显,但是无论如何都需要加载)。因此,如果JIT完全无法避免检查,则成本为1个时钟周期。

一些开销?可以,但是我怀疑您会注意到它。


为了帮助支持我的答案,请参阅Cliff Click博士的博客文章,讨论Java与C的性能。


10

D不允许协变数组。

void main()
{
    class Foo {}
    Object[] a = new Foo[10];
}  

/* Error: cannot implicitly convert expression (new Foo[](10LU)) of type Foo[]
to Object[] */

就像您说的那样,这将是类型系统中的一个漏洞。

您可以原谅这个错误,因为此错误仅在12月13日发布的最新DMD中已得到修复。

D中的数组访问与C或C ++中的数组访问一样快。


根据d-programming-language.org/arrays.html “静态数组T [dim]可以隐式转换为以下之一:... U [] ...动态数组T []可以隐式转换到以下之一:U [] ...其中U是T的基类。”
Ben Voigt 2012年

1
@BenVoigt:过时的文档。
BCS 2012年

1
@BenVoigt:我创建了一个拉取请求来更新文档。希望这会尽快解决。感谢您指出。
彼得·亚历山大

5

根据我在廉价笔记本电脑上所做的测试,使用int[]和之间的差异Integer[]约为1.0 ns。差异可能是由于对类型的额外检查。

通常,当不是每隔ns计数时,对象仅用于更高级别的逻辑。如果您需要保存每个ns,我将避免使用像Objects这样的高级结构。在任何实际程序中,仅分配就可能很小。例如,在同一台计算机上创建一个新对象为5 ns。

调用compareTo可能会更加昂贵,特别是如果您使用诸如String之类的复杂对象。


2

您问过其他现代OO语言吗?好吧,Delphi通过string成为原始对象而不是对象,完全避免了这个问题。因此,字符串数组就是字符串数组,别无其他,对它们的任何操作都与本机代码一样快,而没有类型检查的开销。

但是,字符串数组并不经常使用。Delphi程序员倾向于青睐TStringList此类。它以最小的开销提供了一组字符串组方法,这些方法在许多情况下都非常有用,以至于将该类与瑞士军刀进行了比较。因此,使用字符串列表对象而不是字符串数组是一种习惯。

对于其他对象,通常不存在此问题,因为在Delphi中(如在C ++中),数组不是协变的,以防止此处描述的类型系统漏洞。


1

还是我必须忍受这样的事实,例如,Quicksort总是执行O(n log n)不必要的运行时检查?

CPU性能不是单调的,这意味着较长的程序可以比较短的程序更快(这取决于CPU,对于常见的x86和amd64架构也是如此)。因此,对数组执行绑定检查的程序实际上可能比通过删除这些绑定检查从上一个程序推导的程序更快。

此行为的原因是,边界检查会修改内存中代码的对齐方式,还会更改缓存命中的频率等。

因此,可以接受,Quicksort总是进行O(n log n)伪检查并在分析后进行优化。


1

Scala是一种OO语言,具有不变的数组,而不是协变的数组。它以JVM为目标,因此没有任何性能上的优势,但是它避免了Java和C#共同的功能缺陷,后者由于向后兼容而损害了它们的类型安全性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.