-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds performance optimizations for ExprCallDynamic #1388
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,20 +21,21 @@ import org.partiql.value.PartiQLValueType | |
*/ | ||
@OptIn(PartiQLValueExperimental::class, FnExperimental::class) | ||
internal class ExprCallDynamic( | ||
private val candidates: List<Candidate>, | ||
candidates: Array<Candidate>, | ||
private val args: Array<Operator.Expr> | ||
) : Operator.Expr { | ||
|
||
private val candidateIndex = CandidateIndex.All(candidates) | ||
|
||
override fun eval(env: Environment): PartiQLValue { | ||
val actualArgs = args.map { it.eval(env) }.toTypedArray() | ||
candidates.forEach { candidate -> | ||
if (candidate.matches(actualArgs)) { | ||
return candidate.eval(actualArgs, env) | ||
} | ||
val actualTypes = actualArgs.map { it.type } | ||
candidateIndex.get(actualTypes)?.let { | ||
return it.eval(actualArgs, env) | ||
} | ||
val errorString = buildString { | ||
val argString = actualArgs.joinToString(", ") | ||
append("Could not dynamically find function for arguments $argString in $candidates.") | ||
append("Could not dynamically find function (${candidateIndex.name}) for arguments $argString.") | ||
} | ||
throw TypeCheckException(errorString) | ||
} | ||
|
@@ -47,13 +48,11 @@ internal class ExprCallDynamic( | |
* | ||
* @see ExprCallDynamic | ||
*/ | ||
internal class Candidate( | ||
data class Candidate( | ||
val fn: Fn, | ||
val coercions: Array<Ref.Cast?> | ||
) { | ||
|
||
private val signatureParameters = fn.signature.parameters.map { it.type }.toTypedArray() | ||
|
||
fun eval(originalArgs: Array<PartiQLValue>, env: Environment): PartiQLValue { | ||
val args = originalArgs.mapIndexed { i, arg -> | ||
when (val c = coercions[i]) { | ||
|
@@ -63,32 +62,156 @@ internal class ExprCallDynamic( | |
}.toTypedArray() | ||
return fn.invoke(args) | ||
} | ||
} | ||
|
||
private sealed interface CandidateIndex { | ||
|
||
public fun get(args: List<PartiQLValueType>): Candidate? | ||
johnedquinn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Preserves the original ordering of the passed-in candidates while making it faster to lookup matching | ||
* functions. Utilizes both [Direct] and [Indirect]. | ||
* | ||
* Say a user passes in the following ordered candidates: | ||
* [ | ||
* foo(int16, int16) -> int16, | ||
* foo(int32, int32) -> int32, | ||
* foo(int64, int64) -> int64, | ||
* foo(string, string) -> string, | ||
* foo(struct, struct) -> struct, | ||
* foo(numeric, numeric) -> numeric, | ||
* foo(int64, dynamic) -> dynamic, | ||
* foo(struct, dynamic) -> dynamic, | ||
* foo(bool, bool) -> bool | ||
* ] | ||
* | ||
* With the above candidates, the [CandidateIndex.All] will maintain the original ordering by utilizing: | ||
* - [CandidateIndex.Direct] to match hashable runtime types | ||
* - [CandidateIndex.Indirect] to match the dynamic type | ||
* | ||
* For the above example, the internal representation of [CandidateIndex.All] is a list of | ||
* [CandidateIndex.Direct] and [CandidateIndex.Indirect] that looks like: | ||
* ALL listOf( | ||
* DIRECT hashMap( | ||
* [int16, int16] --> foo(int16, int16) -> int16, | ||
* [int32, int32] --> foo(int32, int32) -> int32, | ||
* [int64, int64] --> foo(int64, int64) -> int64 | ||
* [string, string] --> foo(string, string) -> string, | ||
* [struct, struct] --> foo(struct, struct) -> struct, | ||
* [numeric, numeric] --> foo(numeric, numeric) -> numeric | ||
* ), | ||
* INDIRECT listOf( | ||
* foo(int64, dynamic) -> dynamic, | ||
* foo(struct, dynamic) -> dynamic | ||
* ), | ||
* DIRECT hashMap( | ||
* [bool, bool] --> foo(bool, bool) -> bool | ||
* ) | ||
* ) | ||
* | ||
* @param candidates | ||
*/ | ||
class All( | ||
candidates: Array<Candidate>, | ||
) : CandidateIndex { | ||
|
||
private val lookups: List<CandidateIndex> | ||
internal val name: String = candidates.first().fn.signature.name | ||
|
||
internal fun matches(inputs: Array<PartiQLValue>): Boolean { | ||
for (i in inputs.indices) { | ||
val inputType = inputs[i].type | ||
val parameterType = signatureParameters[i] | ||
val c = coercions[i] | ||
when (c) { | ||
// coercion might be null if one of the following is true | ||
// Function parameter is ANY, | ||
// Input type is null | ||
// input type is the same as function parameter | ||
null -> { | ||
if (!(inputType == parameterType || inputType == PartiQLValueType.NULL || parameterType == PartiQLValueType.ANY)) { | ||
return false | ||
init { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious if we see any notable CPU utilization jump from this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call out -- in the future, I'd say it'd be a good idea for the For now, we know dynamic is quite expensive for our customers. |
||
val lookupsMutable = mutableListOf<CandidateIndex>() | ||
val accumulator = mutableListOf<Pair<List<PartiQLValueType>, Candidate>>() | ||
|
||
// Indicates that we are currently processing dynamic candidates that accept ANY. | ||
var activelyProcessingAny = true | ||
|
||
candidates.forEach { candidate -> | ||
johnedquinn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Gather the input types to the dynamic invocation | ||
val lookupTypes = candidate.coercions.mapIndexed { index, cast -> | ||
when (cast) { | ||
null -> candidate.fn.signature.parameters[index].type | ||
else -> cast.input | ||
} | ||
} | ||
else -> { | ||
// checking the input type is expected by the coercion | ||
if (inputType != c.input) return false | ||
// checking the result is expected by the function signature | ||
// this should branch should never be reached, but leave it here for clarity | ||
if (c.target != parameterType) error("Internal Error: Cast Target does not match Function Parameter") | ||
val parametersIncludeAny = lookupTypes.any { it == PartiQLValueType.ANY } | ||
// A way to simplify logic further below. If it's empty, add something and set the processing type. | ||
if (accumulator.isEmpty()) { | ||
activelyProcessingAny = parametersIncludeAny | ||
accumulator.add(lookupTypes to candidate) | ||
return@forEach | ||
} | ||
when (parametersIncludeAny) { | ||
true -> when (activelyProcessingAny) { | ||
true -> accumulator.add(lookupTypes to candidate) | ||
false -> { | ||
activelyProcessingAny = true | ||
lookupsMutable.add(Direct.of(accumulator.toList())) | ||
accumulator.clear() | ||
accumulator.add(lookupTypes to candidate) | ||
} | ||
} | ||
false -> when (activelyProcessingAny) { | ||
false -> accumulator.add(lookupTypes to candidate) | ||
true -> { | ||
activelyProcessingAny = false | ||
lookupsMutable.add(Indirect(accumulator.toList())) | ||
accumulator.clear() | ||
accumulator.add(lookupTypes to candidate) | ||
} | ||
} | ||
} | ||
} | ||
// Add any remaining candidates (that we didn't submit due to not ending while switching) | ||
when (accumulator.isEmpty()) { | ||
true -> { /* Do nothing! */ } | ||
false -> when (activelyProcessingAny) { | ||
true -> lookupsMutable.add(Indirect(accumulator.toList())) | ||
false -> lookupsMutable.add(Direct.of(accumulator.toList())) | ||
} | ||
} | ||
this.lookups = lookupsMutable | ||
} | ||
|
||
override fun get(args: List<PartiQLValueType>): Candidate? { | ||
return this.lookups.firstNotNullOfOrNull { it.get(args) } | ||
} | ||
} | ||
|
||
/** | ||
* An O(1) structure to quickly find directly matching dynamic candidates. This is specifically used for runtime | ||
* types that can be matched directly. AKA int32, int64, etc. This does NOT include [PartiQLValueType.ANY]. | ||
*/ | ||
data class Direct private constructor(val directCandidates: HashMap<List<PartiQLValueType>, Candidate>) : CandidateIndex { | ||
|
||
companion object { | ||
internal fun of(candidates: List<Pair<List<PartiQLValueType>, Candidate>>): Direct { | ||
val candidateMap = java.util.HashMap<List<PartiQLValueType>, Candidate>() | ||
candidateMap.putAll(candidates) | ||
return Direct(candidateMap) | ||
} | ||
} | ||
|
||
override fun get(args: List<PartiQLValueType>): Candidate? { | ||
return directCandidates[args] | ||
} | ||
johnedquinn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Holds all candidates that expect a [PartiQLValueType.ANY] on input. This maintains the original | ||
* precedence order. | ||
*/ | ||
data class Indirect(private val candidates: List<Pair<List<PartiQLValueType>, Candidate>>) : CandidateIndex { | ||
override fun get(args: List<PartiQLValueType>): Candidate? { | ||
candidates.forEach { (types, candidate) -> | ||
for (i in args.indices) { | ||
if (args[i] != types[i] && types[i] != PartiQLValueType.ANY) { | ||
return@forEach | ||
} | ||
} | ||
return candidate | ||
} | ||
return null | ||
} | ||
return true | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I say create the index in the Compiler and pass to the ExprCallDynamic. In this scenario, we're passing an arg into the constructor just to be passed to another constructor.