Skip to content

Use "call" instead of "callBy" as much as possible to speed up the call to "KFunction". #403

@k163377

Description

@k163377

Use case
If the arguments are fully specified (= don't have to use Default arguments), can speed up the call to jackson-module-kotlin by making a call with KFunction.call.

Describe the solution you'd like
Currently jackson-module-kotlin does mapping by calling KFunction.callBy.
On the other hand, KFunction.callBy has about 1/5 the performance of KFunction.call, so you can expect speedup by doing KFunction.call as much as possible.

Describe alternatives you've considered
For example, you can switch between call and callBy with low overhead by using the following data structure.

import kotlin.reflect.KParameter

internal class ArgumentBucket(private val parameters: List<KParameter>) : Map<KParameter, Any?> {
    val valueArray: Array<Any?> = arrayOfNulls(parameters.size)
    private val initializationStatuses: MutableSet<Int> = HashSet(parameters.size)

    class Entry internal constructor(
            override val key: KParameter,
            override var value: Any?
    ) : Map.Entry<KParameter, Any?>

    operator fun set(key: KParameter, value: Any?): Any? {
        return valueArray[key.index].apply {
            valueArray[key.index] = value
            initializationStatuses.add(key.index)
        }
    }

    fun isFullInitialized(): Boolean = parameters.size == initializationStatuses.size

    override val entries: Set<Map.Entry<KParameter, Any?>>
        get() = valueArray.withIndex()
                .filter { (i, _) -> initializationStatuses.contains(i) }
                .map { (i, arg) -> Entry(parameters[i], arg) }
                .toSet()
    override val keys: Set<KParameter>
        get() = parameters
                .filterIndexed { i, _ -> initializationStatuses.contains(i) }
                .toSet()
    override val size: Int
        get() = initializationStatuses.size
    override val values: Collection<Any?>
        get() = valueArray.filterIndexed { i, _ -> initializationStatuses.contains(i) }

    override fun containsKey(key: KParameter): Boolean = initializationStatuses.contains(key.index)

    override fun containsValue(value: Any?): Boolean = valueArray.withIndex()
            .any { (i, arg) -> initializationStatuses.contains(i) && value == arg }

    override fun get(key: KParameter): Any? = valueArray[key.index]

    override fun isEmpty(): Boolean = initializationStatuses.isEmpty()
}

This is used as follows (rewriting from line 134 of KotlinValueInstantiator.kt).

ArgumentBucket(callable.parameters).apply {
    callableParameters.forEachIndexed { idx, paramDef ->
        if (paramDef != null) {
            this[paramDef] = jsonParamValueList[idx]
        }
    }
}.let {
    if (it.isFullInitialized())
        callable.call(*it.valueArray)
    else
        callable.callBy(it)
}

Assuming that there are few situations where Default arguments is needed, I think that this alone can speed up the process.

Additional context
I have published the contents as ProjectMapK/FastKFunction that incorporates further speed-up methods such as avoiding the spread operator and calling Java reflection directly, so I hope you can refer to that as well.

This is my first time posting issue and I am not good at English, so I'm sorry if there is something wrong with it.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions