8

我有一个在低延迟环境中运行的(java)应用程序,它通常处理约 600 微秒(+/- 100)的指令。自然地,随着我们进一步进入微秒空间,您会看到成本延迟发生变化的事情,现在我们注意到 2/3 的时间花在了 2 个核心域对象的分配上。

基准测试已将有问题的代码部分从现有引用中分离出来,即从字面上构建对象,即基本上是大量引用(每个类中约 15 个)和几个新列表,尽管请参阅下面的注释以了解准确测量的内容这里。

每个人始终需要〜100micros,这对我来说是莫名其妙的,我正试图找出原因。一个快速的基准测试表明,一个类似大小的充满字符串的对象大约需要 2-3 微秒才能新建,显然这种基准测试充满了困难,但认为它可能作为基准有用。

这里有2个Q

  • 如何调查这种行为?
  • 分配缓慢有什么解释?

请注意,所涉及的硬件是 Sun X4600 上的 Solaris 10 x86,具有 8 个双核 opterons @ 3.2GHz

我们看过的东西包括

  • 检查 PrintTLAB 统计信息,显示 v 几个缓慢的分配,所以那里不应该有争用。
  • PrintCompilation 表明其中一段代码不是 JIT 友好的,尽管 Solaris 在这里似乎有一些不寻常的行为(即与现代 linux 相比,现在没有与 solaris10 类似的老式 linux 可供使用)
  • 日志编译...至少可以说有点难以解析,所以这是一项持续的工作,到目前为止还没有什么明显的
  • JVM 版本... 6u6 和 6u14 一致,尚未尝试 6u18 或最新 7

任何和所有想法表示赞赏

对各种帖子的评论摘要,以尝试使事情更清晰

  • 我正在测量的成本是创建通过 Builder(如其中之一)构建的对象的总成本,私有构造函数调用 new ArrayList 几次以及设置对现有对象的引用。测量的成本包括设置构建器和将构建器转换为域对象的成本
  • 编译(通过热点)有显着的影响,但它仍然相对较慢(在这种情况下,编译将其从 100 微秒降至约 60 微秒)
  • 在我的幼稚基准上编译(通过热点)将分配时间从 ~2micros 减少到 ~300ns
  • 延迟不随年轻一代收集算法(ParNew 或并行清除)而变化
4

5 回答 5

3

由于您的问题更多是关于如何调查问题而不是“我的问题是什么”,因此我将坚持使用一些工具进行尝试。

一个非常有用的工具,可以更好地了解正在发生的事情以及BTrace的时间。它类似于 DTrace,但它是一个纯 Java 工具。在那张纸条上,我假设您知道 DTrace,如果不是,那也很有用,如果不是钝的话。这些将使您了解正在发生的事情以及 JVM 和操作系统中的时间。

哦,还有一件事要在您的原始帖子中澄清。你在运行什么收集器?我假设您正在使用像 CMS 这样的低暂停收集器存在高延迟问题。如果是这样,您是否尝试过任何调整?

于 2009-11-18T13:45:36.290 回答
3

当您多次重复同一任务时,您的 CPU 往往会非常高效地运行。这是因为您的缓存未命中时间和 CPU 的预热不会成为一个因素。您也可能没有考虑您的 JVM 预热时间。

如果您在 JVM 和/或 CPU 未预热时尝试相同的操作。你会得到非常不同的结果。

尝试在测试之间做同样的事情说 25 次(小于你的编译阈值)和 sleep(100)。您应该期望看到更高的时间,更接近您在实际应用程序中看到的时间。

您的应用程序的行为会有所不同,但为了说明我的观点。我发现等待 IO 可能比普通睡眠更具破坏性。

当你执行你的基准测试时,你应该尽量确保你是在与同类进行比较。

import java.io.*;
import java.util.Date;

/**
Cold JVM with a Hot CPU took 123 us average
Cold JVM with a Cold CPU took 403 us average
Cold JVM with a Hot CPU took 314 us average
Cold JVM with a Cold CPU took 510 us average
Cold JVM with a Hot CPU took 316 us average
Cold JVM with a Cold CPU took 514 us average
Cold JVM with a Hot CPU took 315 us average
Cold JVM with a Cold CPU took 545 us average
Cold JVM with a Hot CPU took 321 us average
Cold JVM with a Cold CPU took 542 us average
Hot JVM with a Hot CPU took 44 us average
Hot JVM with a Cold CPU took 111 us average
Hot JVM with a Hot CPU took 32 us average
Hot JVM with a Cold CPU took 96 us average
Hot JVM with a Hot CPU took 26 us average
Hot JVM with a Cold CPU took 80 us average
Hot JVM with a Hot CPU took 26 us average
Hot JVM with a Cold CPU took 90 us average
Hot JVM with a Hot CPU took 25 us average
Hot JVM with a Cold CPU took 98 us average
 */
public class HotColdBenchmark {
    public static void main(String... args) {
        // load all the classes.
        performTest(null, 25, false);
        for (int i = 0; i < 5; i++) {
            // still pretty cold
            performTest("Cold JVM with a Hot CPU", 25, false);
            // still pretty cold
            performTest("Cold JVM with a Cold CPU", 25, true);
        }

        // warmup the JVM
        performTest(null, 10000, false);
        for (int i = 0; i < 5; i++) {
            // warmed up.
            performTest("Hot JVM with a Hot CPU", 25, false);
            // bit cold
            performTest("Hot JVM with a Cold CPU", 25, true);
        }
    }

    public static long performTest(String report, int n, boolean sleep) {
        long time = 0;
        long ret = 0;
        for (int i = 0; i < n; i++) {
            long start = System.nanoTime();
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(new Date());
                oos.close();
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
                Date d = (Date) ois.readObject();
                ret += d.getTime();
                time += System.nanoTime() - start;
                if (sleep) Thread.sleep(100);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
        if (report != null) {
            System.out.printf("%s took %,d us average%n", report, time / n / 1000);
        }
        return ret;
    }
}
于 2010-01-06T21:01:09.183 回答
2

只是一些疯狂的猜测:

据我了解,Java VM 处理短期对象的内存与长期对象不同。在我看来,当一个对象从具有一个函数局部引用变为在全局堆中具有引用时,这将是一件大事,这似乎是合理的。它现在必须由 GC 跟踪,而不是在函数退出时可用于清理。

或者可能是从一个引用到对单个对象的多个引用必须更改 GC 记帐。只要一个对象只有一个引用,就很容易清理。多个引用可以有引用循环和/或 GC 可能必须在所有其他对象中搜索引用。

于 2009-12-23T15:54:30.170 回答
2

内存分配可能会导致副作用。内存分配是否可能导致堆被压缩?您是否查看过您的内存分配是否导致 GC 同时运行?

您是否分别计时了创建新的 ArrayLists 需要多长时间?

于 2009-11-18T11:06:42.403 回答
2

即使使用如此出色的硬件,在通用操作系统上运行的通用 VM 也可能无法获得微秒级延迟保证。巨大的吞吐量是您所希望的最好的。如果你需要一个实时虚拟机如何切换(我说的是 RTSJ 和所有这些......)

...我的两分钱

于 2009-11-18T11:12:05.420 回答