草庐IT

java - 为什么ByteBuffer.allocate()和ByteBuffer.allocateDirect()之间的奇异性能曲线差异

coder 2023-05-15 原文

我正在研究一些SocketChannelSocketChannel的代码,这些代码最适合直接字节缓冲区-长寿且很大(每个连接数十到数百兆字节)。在用FileChannel散列确切的循环结构时,我运行了关于ByteBuffer.allocate()ByteBuffer.allocateDirect()性能的一些微基准测试。

结果令人惊讶,我无法真正解释。在下图中,对于ByteBuffer.allocate()传输实现,在256KB和512KB处有一个非常明显的悬崖-性能下降了约50%! ByteBuffer.allocateDirect()似乎也有较小的性能下降。 (%增益系列有助于可视化这些更改。)

缓冲区大小(字节)与时间(MS)的比较

为什么ByteBuffer.allocate()ByteBuffer.allocateDirect()之间的奇数性能曲线有所不同? 幕后到底发生了什么?

可能与硬件和操作系统有关,因此非常详细,因此以下是这些详细信息:

  • 带双核Core 2 CPU的MacBook Pro
  • 英特尔X25M SSD驱动器
  • OSX 10.6.4

  • 源代码,根据要求:
    package ch.dietpizza.bench;
    
    import static java.lang.String.format;
    import static java.lang.System.out;
    import static java.nio.ByteBuffer.*;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.UnknownHostException;
    import java.nio.ByteBuffer;
    import java.nio.channels.Channels;
    import java.nio.channels.ReadableByteChannel;
    import java.nio.channels.WritableByteChannel;
    
    public class SocketChannelByteBufferExample {
        private static WritableByteChannel target;
        private static ReadableByteChannel source;
        private static ByteBuffer          buffer;
    
        public static void main(String[] args) throws IOException, InterruptedException {
            long timeDirect;
            long normal;
            out.println("start");
    
            for (int i = 512; i <= 1024 * 1024 * 64; i *= 2) {
                buffer = allocateDirect(i);
                timeDirect = copyShortest();
    
                buffer = allocate(i);
                normal = copyShortest();
    
                out.println(format("%d, %d, %d", i, normal, timeDirect));
            }
    
            out.println("stop");
        }
    
        private static long copyShortest() throws IOException, InterruptedException {
            int result = 0;
            for (int i = 0; i < 100; i++) {
                int single = copyOnce();
                result = (i == 0) ? single : Math.min(result, single);
            }
            return result;
        }
    
    
        private static int copyOnce() throws IOException, InterruptedException {
            initialize();
    
            long start = System.currentTimeMillis();
    
            while (source.read(buffer)!= -1) {    
                buffer.flip();  
                target.write(buffer);
                buffer.clear();  //pos = 0, limit = capacity
            }
    
            long time = System.currentTimeMillis() - start;
    
            rest();
    
            return (int)time;
        }   
    
    
        private static void initialize() throws UnknownHostException, IOException {
            InputStream  is = new FileInputStream(new File("/Users/stu/temp/robyn.in"));//315 MB file
            OutputStream os = new FileOutputStream(new File("/dev/null"));
    
            target = Channels.newChannel(os);
            source = Channels.newChannel(is);
        }
    
        private static void rest() throws InterruptedException {
            System.gc();
            Thread.sleep(200);      
        }
    }
    

    最佳答案

    ByteBuffer的工作方式以及为什么Direct(Byte)Buffers现在才是真正有用的。

    首先,我有点惊讶,这不是常识,但要带我

    直接字节缓冲区在Java堆外部分配地址。

    这是最重要的:所有OS(和 native C)功能都可以利用该地址,而无需将对象锁定在堆上并复制数据。复制的简短示例:为了通过Socket.getOutputStream()。write(byte [])发送任何数据, native 代码必须“锁定” byte [],将其复制到Java堆外,然后调用OS函数,例如send。复制是在堆栈上执行(对于较小的byte []),或者通过malloc/free进行复制(对于较大的字节)。
    DatagramSockets没有什么不同,它们也可以复制-除了它们限于64KB并分配在堆栈上,如果线程堆栈不够大或深度递归,它们甚至可以杀死进程。
    注意:锁定可防止JVM/GC在堆周围移动/重新分配对象

    因此,在引入NIO的前提下,避免了复制和大量流传输/间接调用。在数据到达目的地之前,通常有3-4种缓冲类型的流。 (是的,波兰以出色的射门扳平了(!))
    通过引入直接缓冲区,java可以直接与C native 代码通信,而无需任何必要的锁定/复制。因此,sent函数可以将缓冲区的地址添加到该位置,并且性能与 native C几乎相同。
    那就是直接缓冲区。

    有直接缓冲区的主要问题-对于allocate and expensive to deallocate来说它们很昂贵,并且使用起来很麻烦,没有像byte []这样的东西。

    非直接缓冲区不能提供直接缓冲区的真正本质-即直接桥接到 native /OS,而它们是轻量级的并且共享完全相同的API-甚至,它们可以wrap byte[]甚至它们的支持数组都可用直接操纵-不爱什么?好吧,他们必须被复制!

    因此,由于OS/native不能自然地使用em,因此Sun/Oracle如何处理非直接缓冲区。使用非直接缓冲区时,必须创建直接计数器部分。该实现足够聪明,可以使用ThreadLocal并通过SoftReference *缓存一些直接缓冲区,从而避免了高昂的创建成本。天真的部分是在复制它们时出现的-每次尝试复制整个缓冲区(remaining())。

    现在想像一下:512 KB非直接缓冲区将变为64 KB套接字缓冲区,套接字缓冲区占用的空间不会超过其大小。因此,第一次将512 KB从非直接复制到线程本地直接,但仅会使用其中的64 KB。下次将复制512-64 KB,但仅使用64 KB,第三次将复制512-64 * 2 KB,但仅使用64 KB,依此类推...这是乐观的,因为总是套接字缓冲区将完全为空。因此,您不仅要总共复制n KB,而且要复制n×n÷m(n = 512,m = 16(套接字缓冲区剩余的平均空间))。

    复制部分是所有非直接缓冲区的公共(public)/抽象路径,因此实现永远不知道目标容量。复制会破坏缓存,而不会破坏缓存,减少内存带宽等。

    *有关SoftReference缓存的说明:它取决于GC的实现,并且体验可能会有所不同。 Sun的GC使用可用的堆内存来确定SoftRefence的生命周期,这将导致释放时有些尴尬的行为-应用程序需要再次分配以前缓存的对象-即分配更多(直接ByteBuffer在堆中占很小的一部分,因此至少它们不会影响额外的缓存垃圾,而是会受到影响)

    我的经验法则-池直接缓冲区的大小与套接字读/写缓冲区的大小相同。操作系统绝不会复制过多的内容。

    此微基准测试主要是内存吞吐量测试,操作系统会将文件完全放在缓存中,因此它主要测试memcpy。一旦缓冲区用完了L2缓存,性能下降就会很明显。同样地运行基准会增加和累积GC收集成本。 (rest()不会收集软引用的ByteBuffers)

    关于java - 为什么ByteBuffer.allocate()和ByteBuffer.allocateDirect()之间的奇异性能曲线差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3651737/

    有关java - 为什么ByteBuffer.allocate()和ByteBuffer.allocateDirect()之间的奇异性能曲线差异的更多相关文章

    1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

      我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

    3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

      我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

    5. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

      为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

    6. ruby-on-rails - Rails 应用程序之间的通信 - 2

      我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

    7. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

      它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

    8. ruby - Infinity 和 NaN 的类型是什么? - 2

      我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

    9. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

      如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

    10. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

      关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

    随机推荐