寻找一种 GC 友好的方式来频繁地替换子字符串

Che*_*nhe 1 string stringbuilder android garbage-collection kotlin

我的目标是简单地替换子字符串,但非常频繁。该程序在Android中运行。

比如我有一个 string ={a} is a good {b}.和一个 map= {{a}=Bob, {b}=boy},结果应该是Bob is a good boy.我需要处理不同字符串的这种替换最多 400 次对等秒,因为 map 的值会实时更新。

但是我使用 trie 树和 Aho-Corasick 自动机来获得高性能,这是核心片段:

    val builder: StringBuilder

    private fun replace(str: String): String {
        if (!getFail) {
            getFail()
        }
        var p = 1
        builder.setLength(0)
        for (c in str) {
            builder.append(c)
            if (c.toInt() !in 0..126) {
                continue // ignore non-ascii char
            }
            var k = trie[p][c.toInt()]
            while (k > 1) {
                // find a tag
                if (end[k] != 0) {
                    val last = builder.length - end[k]
                    // replace the tag
                    values[builder.sub(last, end[k])]?.let {
                        builder.replace1(last, end[k], it)
                    }
                    p = 0
                    break
                }
                k = fail[k] // not find
            }
            p = trie[p][c.toInt()]
        }
        return builder.toString()
    }
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我已经习惯StringBuilder了重用内存,但最后我不得不调用StringBuilder.toString()返回结果,这个操作创建了一个新的字符串对象。同时结果的生命周期很短,替换函数被频繁调用。因此,JVM 会频繁地进行 GC。

有什么方法可以重用短寿命结果字符串占用的内存?或者只是其他一些解决方案。

Ste*_*n C 5

有什么方法可以重用短寿命结果字符串占用的内存?

不。

或者只是其他一些解决方案。

如果您可以更改使用String此方法生成的对象的代码以接受 a CharSequence。然后你可以将它的StringBuilder实例传递给builder,并避免toString()调用。

问题是,你将无法阻止的东西从铸造CharSequenceStringBuilder和变异它。(但如果代码不是安全关键的,你可以忽略它。意外地很难做到这一点,特别是如果你CharSequence在传递时使用接口类型StringBuilder。)

另一个问题是调用者实际上每次都会以不同的状态获取相同的对象。它无法保持状态……除非它要求toString()它。


但是您可能会不必要地担心性能。GC 比较擅长处理生命周期较短的对象。假设一个对象在创建后的第一个 GC 周期中无法访问,它永远不会被标记或复制,删除它的成本将为零。粗略估计,“来自”空间中的可到达对象将花费您。

我会首先做一些分析和 GC 监控。如果有明确的证据表明短期字符串导致性能问题,则仅按照上述方式更改代码。

(我的直觉是每秒 400 个短期字符串应该不是问题,假设 1)它们不是很大并且 2)您选择了适合您的用例的 GC。)

  • 由于这个问题是关于“GC 友好的方式”,因此必须强调,保留对 StringBuilder 的引用并重用它,与“GC 友好”相反。返回一个“CharSequence”在这里是有道理的。最好的解决方案是在每次调用时创建一个新的“StringBuilder”,但返回“StringBuilder”(作为“CharSequence”),以避免从构建器构造新“String”时发生的内部复制操作。您甚至可以通过“CharBuffer.wrap(CharSequence)”来保护它,以获得真正只读的“CharSequence”,而无需复制开销。 (2认同)
  • @Chenhe那么你有一个[xy问题](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378)。“减少开销”是一个实际的目标。“最小化暂停时间”或“最大化吞吐量”是实际目标。“避免 GC”作为目的本身并不是一个有意义的目标。“内存搅动”甚至根本不是一个有意义的技术术语。 (2认同)
  • @Chenhe - 如果您询问 Android GC 行为,那么您错误地标记了您的问题。该文档中给出的建议是针对 Android 平台量身定制的;即更简单的 GC 和更少的可用内存。如果您使用 Oracle / OpenJDK,您应该查阅 Oracle 文档。 (2认同)
  • 而“事实上”并不一定是真的。短期的“StringBuilder”与短期的“String”具有相同的属性。对于(据我所知)所有现代 Oracle / OpenJDK,“内存搅动”仅在过度(例如每秒数百万个对象)或必须满足严格的低延迟要求时才重要。你所做的是过早的优化......这可能是浪费你的时间。 (2认同)
  • 快速分配缩短 GC 间隔的事实是微不足道的,但并不取决于对象的数量,而是取决于每次执行(可能是隐藏的)复制时的*内存量*,内存量随着字符串的大小而缩放手术。这就是为什么我在之前的评论中将重点放在复制操作上。它们在执行时会花费一次(根据字符串大小进行缩放),并且它们比简单的包装对象(同样,根据字符串大小进行缩放)更能缩短 GC 运行之间的时间。相反,正如 Stephen C 所说,构造“StringBuilder”或“String”并不重要。 (2认同)