Use CompositeDecoder::decodeCollectionSize to pre-allocate collections#3192
Use CompositeDecoder::decodeCollectionSize to pre-allocate collections#3192fzhinkin wants to merge 12 commits into
Conversation
1a41b4d to
c042815
Compare
|
Raw benchmarking results: |
| buffer[position++] = c | ||
| } | ||
|
|
||
| override fun build() = buffer.copyOf(position) |
There was a problem hiding this comment.
To discuss: we can avoid a copy here if position == buffer.size, but we need to explicitly invalidate the builder, JIC.
There was a problem hiding this comment.
builders do not live outside of merge() IIRC
| val compositeDecoder = decoder.beginStructure(descriptor) | ||
| val expectedSize = compositeDecoder.decodeCollectionSize(descriptor) | ||
| val builder = if (previous != null) { | ||
| previous.toBuilder().also { it.checkCapacity(expectedSize) } |
There was a problem hiding this comment.
This should add the new new "expectedSize" to the current size when calling checkCapacity. The common case for this branch is for incremental parsing of list elements.
It also makes sense to pass the new needed size to toBuilder to avoid an extra copy in the case that a new builder is actually created (not default behaviour).
There was a problem hiding this comment.
Nice catch, thanks for pointing to it!
Not only the code does not work as expected, it can also fail in runtime if checkCapacity can't handle negative expectedSize (which is the case for all non-empty implementations, I guess).
|
Updated benchmarking results: |
| override fun Array<Element>.toBuilder(expectedAdditionalSize: Int): ArrayList<Element> { | ||
| checkBuildersInitialCapacity(expectedAdditionalSize) | ||
| val expectedCapacity = size + expectedAdditionalSize.coerceAtLeast(0) | ||
| return ArrayList<Element>(expectedAdditionalSize).also { it.addAll(this) } |
There was a problem hiding this comment.
Should be ArrayList<Element>(expectedCapacity), right?
| val builder = previous?.toBuilder() ?: builder() | ||
| val startIndex = builder.builderSize() | ||
| val compositeDecoder = decoder.beginStructure(descriptor) | ||
| val expectedSize = compositeDecoder.decodeCollectionSize(descriptor) |
There was a problem hiding this comment.
I suggest checking that it is not less than -1 right here instead of calling checkBuildersInitialCapacity(initialCapacityHint) in every implementation
There was a problem hiding this comment.
Apparently, I was in the "let's write over 9000 LOC" mood. :/
| val compositeDecoder = decoder.beginStructure(descriptor) | ||
| val expectedSize = compositeDecoder.decodeCollectionSize(descriptor) | ||
| val builder = if (previous != null) { | ||
| previous.toBuilder(expectedSize) |
There was a problem hiding this comment.
I think passing here previous.toBuider(previous.collectionSize() + expectedSize.coerceAtLeast(0)) would also significantly simplify implementations
There was a problem hiding this comment.
Maybe the coerceAtLeast should be 1 (the idea that this would not be called if there is not at least one element added, and the exception would be much less likely than this case).
There was a problem hiding this comment.
Incoming collection can be empty, there's no need to re-allocate existing one in this case
| buffer[position++] = c | ||
| } | ||
|
|
||
| override fun build() = buffer.copyOf(position) |
There was a problem hiding this comment.
builders do not live outside of merge() IIRC
| override fun Array<Element>.toBuilder(): ArrayList<Element> = ArrayList(this.asList()) | ||
| override fun Array<Element>.toBuilder(expectedAdditionalSize: Int): ArrayList<Element> { | ||
| checkBuildersInitialCapacity(expectedAdditionalSize) | ||
| val expectedCapacity = size + expectedAdditionalSize.coerceAtLeast(0) |
There was a problem hiding this comment.
It may be more optimal to coerce it to at least 1. The use case for extending lists kind of implies to add at least one new element.
| val compositeDecoder = decoder.beginStructure(descriptor) | ||
| val expectedSize = compositeDecoder.decodeCollectionSize(descriptor) | ||
| val builder = if (previous != null) { | ||
| previous.toBuilder(expectedSize) |
There was a problem hiding this comment.
Maybe the coerceAtLeast should be 1 (the idea that this would not be called if there is not at least one element added, and the exception would be much less likely than this case).
|
Nice! BTW it's not only CBOR but any 3rd party format too, like my CSV format that returns a List. |
Reimplemented
AbstractCollectionSerializer::mergeto read expected collection size first and pass it as an initial capacity to a corresponding builder. Builder were updated to take the capacity into account.The change is quite intrusive, but for formats providing information about the collection size before reading it (it's only CBOR for now, though) it reduces the footprint and slightly improve decoding performance.
Fixes #3153