[英]What's the space-efficient way to read multiple ByteBuffers into a single String?
我正在編寫一個解碼器,它將接收一系列字節緩沖區並將內容解碼為單個String
。 可以有任意數量的字節緩沖區,每個緩沖區包含任意數量的字節。 緩沖區不一定在字符邊界上拆分,因此根據編碼,它們可能在開頭或結尾包含部分字符。 這是我想要做的,其中StringByteStreamDecoder
是我需要編寫的新 class 。
suspend fun decode(data: Flow<ByteBuffer>, charset: Charset): String {
val decoder = StringByteStreamDecoder(charset)
data.collect { bytes ->
decoder.feed(bytes)
}
decoder.endOfInput()
return decoder.toString()
}
最簡單的方法是將所有字節緩沖區收集到一個單字節數組中。 我拒絕了這種方法,因為它具有顯着的 memory 開銷。 它至少需要為完整消息分配空間兩次:一次用於原始字節,一次用於解碼字符。 這是我的簡單實現,使用ByteArrayOutputStream
作為擴展字節緩沖區。
class StringByteStreamDecoder(private val charset: Charset) {
private val buffer = ByteArrayOutputStream()
fun feed(data: ByteBuffer) {
if (data.hasArray()) {
buffer.write(data.array(), data.position() + data.arrayOffset(), data.remaining())
} else {
val array = ByteArray(data.remaining())
data.get(array)
buffer.write(array, 0, array.size)
}
}
fun endOfInput() {
buffer.flush()
}
override fun toString(): String {
return buffer.toString(charset)
}
}
為了避免在 memory 中緩沖整個字節 stream,我想即時解碼字符。 不可能將每個字節緩沖區直接解碼為字符數據,因為它可能在開頭和結尾包含部分字符。 字符解碼器(據我所知)沒有緩沖部分字符的能力,只會消耗完整的字符。 因此,對於每個傳入的字節緩沖區,我的方法是:
在接收到所有數據后,可以刷新臨時字節緩沖區中的任何剩余字節。 這解決了部分字符的問題,前提是臨時字節緩沖區至少與字符集的最寬字符一樣大。
class StringByteStreamDecoder(charset: Charset, bufferSize: Int = 1024) {
private val decoder = charset.newDecoder()
private val tmpBytes = ByteBuffer.allocate(bufferSize)
private val tmpChars = CharBuffer.allocate((tmpBytes.capacity() * decoder.maxCharsPerByte()).toInt() + 1)
private val stringBuilder = StringBuilder()
fun feed(data: ByteBuffer) {
do {
tmpBytes.put(data.nextSlice(maxSize = tmpBytes.remaining()))
flushBytes()
} while (data.hasRemaining())
}
fun endOfInput() {
flushBytes(endOfInput = true)
}
override fun toString(): String = stringBuilder.toString()
private fun ByteBuffer.nextSlice(maxSize: Int): ByteBuffer {
val size = minOf(maxSize, remaining())
val slice = slice(position(), size)
position(position() + size)
return slice
}
private fun flushBytes(endOfInput: Boolean = false) {
tmpBytes.flip()
decoder.decode(tmpBytes, tmpChars, endOfInput)
tmpBytes.compact()
flushChars()
}
private fun flushChars() {
tmpChars.flip()
stringBuilder.append(tmpChars)
tmpChars.clear()
}
}
由於額外的臨時緩沖區,我仍然對這種方法並不完全滿意。 我希望能夠使臨時字節緩沖區最多容納一個(部分)字符。 但是,如果我這樣做了,我必須以某種方式將其添加到下一個傳入的數據塊中。 這意味着分配一個新的字節緩沖區來包含緩沖的部分字符和新的傳入數據。 將所有數據從傳入緩沖區復制到連接緩沖區並不比首先使用更大的臨時緩沖區更有效。
但是,對於小字符串,臨時字節緩沖區代表着很大的開銷。 我可以使臨時緩沖區更小,但這可能會在解碼較大的字符串時損害性能。
我也知道StringBuilder
將根據輸入動態調整大小,並且可能不是為結果String
分配空間的最有效方法。
我想我可以避免一些額外的 memory 分配,如果我可以訪問類似這個答案中描述的鏈緩沖區的東西。 這將允許我創建傳入字節緩沖區的串聯窗口視圖。 然后字符解碼器可以直接使用連接的視圖,而不需要額外的臨時緩沖區。 但是,我在標准庫中找不到任何提供這種功能的東西。
是否可以在不分配額外的 memory 超出傳入數據和生成的String
本身的情況下解決此問題? 如果不是,那么需要的額外 memory 的最低數量是多少,達到最低限度的方法是什么?
我懷疑是否有可能在不復制數據的情況下在 Java 中創建一個字符串。 無論是從byte[]
還是從char[]
創建它,我們連接其他字符串,我們使用StringBuilder
/ StringBuffer
- 我們總是必須復制數據。 您似乎錯誤地認為StringBuilder
以某種方式直接創建字符串。 不,它會復制toString()
中的數據。 潛在地, substring()
可以避免在某些 JVM 中創建數據副本,但我不知道它在實踐中是否是這樣實現的。
這很可能是由於字符串保證是不可變的,但數據源通常是可變的,因此需要復制數據。
如果您事先知道數據的大小或懷疑它的大小,我認為最有效的方法是分配字節數組,將所有內容寫入其中然后轉換。 所以你的初步嘗試。
如果 memory 對您來說真的是一個大問題,那么您可以尋找可以讓您訪問一些高級內容的 JVM,也許它們允許從byte[]
/ char[]
創建字符串而無需復制。 但首先,您應該重新考慮是否應該真正關注這一點。 或者這只是一個過早的優化。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.