字符串 vs 字节数组,性能

13
(这篇帖子是关于高频类型编程的)
我最近在一个论坛上看到(我想他们正在讨论Java),如果你必须解析大量的字符串数据,最好使用字节数组而不是使用split()函数的字符串。确切的帖子内容如下:
“与任何语言,包括C++、Java、C#一起工作的性能技巧之一是避免对象创建。这不是分配或GC的成本,而是访问不适合CPU缓存的大型内存数组的成本。
现代CPU比它们的内存快得多。对于每个缓存未命中,它会停滞很多很多个周期。大部分CPU晶体管预算都被分配用来减少这种情况,采用大型缓存和大量时钟。
GPU通过准备大量线程来隐藏内存访问延迟并且几乎没有缓存,并将晶体管花费在更多的核心上来以不同的方式解决问题。
因此,例如,与其使用String和split来解析消息,不如使用可以原地更新的字节数组。你真的想要避免在大型数据结构中进行随机内存访问,至少在内部循环中。”
他是在说“不要使用字符串,因为它们是对象,创建对象会很耗费资源”吗?还是他在说别的什么?
使用字节数组是否能确保数据在缓存中尽可能长时间地保留? 当您使用字符串时,它是否太大而无法保存在CPU缓存中? 通常来说,使用基本数据类型是编写更快代码的最佳方法吗?
2个回答

15
他的意思是,如果你将一段文本分成多个字符串对象,那么这些字符串对象的局部性会比大型文本数组差。每个字符串及其包含的字符数组都会在内存中的不同位置;它们可能分散在各个地方。在处理数据时,内存缓存很可能需要频繁地进出以访问各种字符串。相比之下,一个大的数组具有最好的局部性,因为所有数据都在内存的一个区域中,缓存交换将被最小化。
当然,这也有限制:如果文本非常非常大,并且您只需要解析其中的一部分,则这几个小字符串可能比大块文本更适合缓存。

你说“它们可以分散在各个地方”。字符串的字符是存储在连续内存中还是像链表一样? - user997112
字符在连续的内存中。但通常一个字符串对象由两个独立的块组成:字符串对象本身和一个用于保存字符的数组。如果您创建了许多字符串,则每个字符串及其数组都会存在某个地方,并且不能保证所有这些对象都位于同一内存区域; 每个对象都是单独分配的,可以位于任何位置。在C++中,如果它们在值数组中分配,则字符串对象本身可以全部位于同一位置;在Java中,您甚至没有那个选项。 - Ernest Friedman-Hill
一个字符串内的字符是连续的,但如果你有多个字符串,它们可以分散在各处。如果你在Java中使用String.substring,它是对底层字符串的视图,所以这种情况不会发生,然而C++和C#在从另一个字符串中取子串时会复制源数据。 - Peter Lawrey

3

使用byte[]char*而不是字符串在HFT中有许多其他原因。Java中的字符串由16位char组成,且是不可变的。 byte[]ByteBuffer易于回收,具有良好的缓存位置,可以脱离堆(直接),节省一份拷贝,避免字符编码器。这都假定您正在使用ASCII数据。

char*或ByteBuffers也可以映射到网络适配器以保存另一个副本。(对于ByteBuffers需要进行一些调整)

在HFT中,您很少一次处理大量的数据。理想情况下,您希望在数据通过套接字时立即处理数据,即一次一个数据包(约1.5KB)。


你如何避免字节数组进入堆中,难道不需要在声明时使用'new'吗? - user997112
在C++中,你需要使用new或者malloc。在Java中,你可以使用ByteBuffer.allocateDirect()(它是一个malloc内存块的包装器)。使用反射或JNI,你可以改变address指向的位置,以便直接访问网络适配器(如果你正在使用内核旁路)。如果你使用Unsafe类,完全可以不用ByteBuffer(虽然这很少有足够的差异)。 - Peter Lawrey
亲爱的Peter,你能否详细说明一下“使用反射或JNI可以更改地址指向的位置,以便直接访问网络适配器(如果您正在使用内核旁路)”?你知道有没有一些带有小例子代码的网站吗?我认为这在C++中比Java容易得多。 - user997112
这在C或C++中很容易实现。你只需要一个指针,这是司空见惯的事情。在Java中,你需要跳过一些障碍,但你也可以达到同样的效果。我不确定除了使用反射设置字段之外还能给你展示什么例子。即Field.setLong()。 - Peter Lawrey

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