Merge 'Performance: hoist entire expressions out of hot loops if they are constant' from Jussi Saurio

## Problem:
- We have cases where we are evaluating expressions in a hot loop that
could only be evaluated once. For example: `CAST('2025-01-01' as
DATETIME)` -- the value of this never changes, so we should only run it
once.
- We have no robust way of doing this right now for entire _expressions_
-- the only existing facility we have is
`program.mark_last_insn_constant()`, which has no concept of how many
instructions translating a given _expression_ spends, and breaks very
easily for this reason.
## Main ideas of this PR:
- Add `expr.is_constant()` determining whether the expression is
compile-time constant. Tries to be conservative and not deem something
compile-time constant if there is no certainty.
- Whenever we think a compile-time constant expression is about to be
translated into bytecode in `translate_expr()`, start a so called
`constant span`, which means a range of instructions that are part of a
compile-time constant expression.
- At the end of translating the program, all `constant spans` are
hoisted outside of any table loops so they only get evaluated once.
- The target offsets of any jump instructions (e.g. `Goto`) are moved to
the correct place, taking into account all instructions whose offsets
were shifted due to moving the compile-time constant expressions around.
- An escape hatch wrapper `translate_expr_no_constant_opt()` is added
for cases where we should not hoist constants even if we otherwise
could. Right now the only example of this is cases where we are reusing
the same register(s) in multiple iterations of some kind of loop, e.g.
`VALUES(...)` or in the `coalesce()` function implementation.
## Performance effects
Here is an example of a modified/simplified TPC-H query where the
`CAST()` calls were previously run millions of times in a hot loop, but
now they are optimized out of the loop.
**BYTECODE PLAN BEFORE:**
```sql
limbo> explain select
        l_orderkey,
        3 as revenue,
        o_orderdate,
        o_shippriority
from
        lineitem,
        orders,
        customer
where
        c_mktsegment = 'FURNITURE'
        and c_custkey = o_custkey
        and l_orderkey = o_orderkey
        and o_orderdate < cast('1995-03-29' as datetime)
        and l_shipdate > cast('1995-03-29' as datetime);
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     26    0                    0   Start at 26
1     OpenRead           0     10    0                    0   table=lineitem, root=10
2     OpenRead           1     9     0                    0   table=orders, root=9
3     OpenRead           2     8     0                    0   table=customer, root=8
4     Rewind             0     25    0                    0   Rewind lineitem
5       Column           0     10    5                    0   r[5]=lineitem.l_shipdate
6       String8          0     7     0     1995-03-29     0   r[7]='1995-03-29'
7       Function         0     7     6     cast           0   r[6]=func(r[7..8])  <-- CAST() executed millions of times
8       Le               5     6     24                   0   if r[5]<=r[6] goto 24
9       Column           0     0     9                    0   r[9]=lineitem.l_orderkey
10      SeekRowid        1     9     24                   0   if (r[9]!=orders.rowid) goto 24
11      Column           1     4     10                   0   r[10]=orders.o_orderdate
12      String8          0     12    0     1995-03-29     0   r[12]='1995-03-29'
13      Function         0     12    11    cast           0   r[11]=func(r[12..13])
14      Ge               10    11    24                   0   if r[10]>=r[11] goto 24
15      Column           1     1     14                   0   r[14]=orders.o_custkey
16      SeekRowid        2     14    24                   0   if (r[14]!=customer.rowid) goto 24
17      Column           2     6     15                   0   r[15]=customer.c_mktsegment
18      Ne               15    16    24                   0   if r[15]!=r[16] goto 24
19      Column           0     0     1                    0   r[1]=lineitem.l_orderkey
20      Integer          3     2     0                    0   r[2]=3
21      Column           1     4     3                    0   r[3]=orders.o_orderdate
22      Column           1     7     4                    0   r[4]=orders.o_shippriority
23      ResultRow        1     4     0                    0   output=r[1..4]
24    Next               0     5     0                    0
25    Halt               0     0     0                    0
26    Transaction        0     0     0                    0   write=false
27    String8            0     8     0     DATETIME       0   r[8]='DATETIME'
28    String8            0     13    0     DATETIME       0   r[13]='DATETIME'
29    String8            0     16    0     FURNITURE      0   r[16]='FURNITURE'
30    Goto               0     1     0
```
**BYTECODE PLAN AFTER**:
```sql
limbo> explain select
        l_orderkey,
        3 as revenue,
        o_orderdate,
        o_shippriority
from
        lineitem,
        orders,
        customer
where
        c_mktsegment = 'FURNITURE'
        and c_custkey = o_custkey
        and l_orderkey = o_orderkey
        and o_orderdate < cast('1995-03-29' as datetime)
        and l_shipdate > cast('1995-03-29' as datetime);
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     21    0                    0   Start at 21
1     OpenRead           0     10    0                    0   table=lineitem, root=10
2     OpenRead           1     9     0                    0   table=orders, root=9
3     OpenRead           2     8     0                    0   table=customer, root=8
4     Rewind             0     20    0                    0   Rewind lineitem
5       Column           0     10    5                    0   r[5]=lineitem.l_shipdate
6       Le               5     6     19                   0   if r[5]<=r[6] goto 19
7       Column           0     0     9                    0   r[9]=lineitem.l_orderkey
8       SeekRowid        1     9     19                   0   if (r[9]!=orders.rowid) goto 19
9       Column           1     4     10                   0   r[10]=orders.o_orderdate
10      Ge               10    11    19                   0   if r[10]>=r[11] goto 19
11      Column           1     1     14                   0   r[14]=orders.o_custkey
12      SeekRowid        2     14    19                   0   if (r[14]!=customer.rowid) goto 19
13      Column           2     6     15                   0   r[15]=customer.c_mktsegment
14      Ne               15    16    19                   0   if r[15]!=r[16] goto 19
15      Column           0     0     1                    0   r[1]=lineitem.l_orderkey
16      Column           1     4     3                    0   r[3]=orders.o_orderdate
17      Column           1     7     4                    0   r[4]=orders.o_shippriority
18      ResultRow        1     4     0                    0   output=r[1..4]
19    Next               0     5     0                    0
20    Halt               0     0     0                    0
21    Transaction        0     0     0                    0   write=false
22    String8            0     7     0     1995-03-29     0   r[7]='1995-03-29'
23    String8            0     8     0     DATETIME       0   r[8]='DATETIME'
24    Function           1     7     6     cast           0   r[6]=func(r[7..8]) <-- CAST() executed twice
25    String8            0     12    0     1995-03-29     0   r[12]='1995-03-29'
26    String8            0     13    0     DATETIME       0   r[13]='DATETIME'
27    Function           1     12    11    cast           0   r[11]=func(r[12..13])
28    String8            0     16    0     FURNITURE      0   r[16]='FURNITURE'
29    Integer            3     2     0                    0   r[2]=3
30    Goto               0     1     0                    0
```
**EXECUTION RUNTIME BEFORE:**
```sql
limbo> select
        l_orderkey,
        3 as revenue,
        o_orderdate,
        o_shippriority
from
        lineitem,
        orders,
        customer
where
        c_mktsegment = 'FURNITURE'
        and c_custkey = o_custkey
        and l_orderkey = o_orderkey
        and o_orderdate < cast('1995-03-29' as datetime)
        and l_shipdate > cast('1995-03-29' as datetime);
┌────────────┬─────────┬─────────────┬────────────────┐
│ l_orderkey │ revenue │ o_orderdate │ o_shippriority │
├────────────┼─────────┼─────────────┼────────────────┤
└────────────┴─────────┴─────────────┴────────────────┘
Command stats:
----------------------------
total: 3.633396667 s (this includes parsing/coloring of cli app)
```
**EXECUTION RUNTIME AFTER:**
```sql
limbo> select
        l_orderkey,
        3 as revenue,
        o_orderdate,
        o_shippriority
from
        lineitem,
        orders,
        customer
where
        c_mktsegment = 'FURNITURE'
        and c_custkey = o_custkey
        and l_orderkey = o_orderkey
        and o_orderdate < cast('1995-03-29' as datetime)
        and l_shipdate > cast('1995-03-29' as datetime);
┌────────────┬─────────┬─────────────┬────────────────┐
│ l_orderkey │ revenue │ o_orderdate │ o_shippriority │
├────────────┼─────────┼─────────────┼────────────────┤
└────────────┴─────────┴─────────────┴────────────────┘
Command stats:
----------------------------
total: 2.0923475 s (this includes parsing/coloring of cli app)
````

Reviewed-by: Pere Diaz Bou <pere-altea@homail.com>

Closes #1359
This commit is contained in:
Jussi Saurio 2025-04-25 16:55:41 +03:00
commit fe65d6e991
15 changed files with 640 additions and 238 deletions

View file

@ -1,6 +1,6 @@
use std::{
cell::Cell,
collections::HashMap,
cmp::Ordering,
rc::{Rc, Weak},
sync::Arc,
};
@ -16,24 +16,25 @@ use crate::{
Connection, VirtualTable,
};
use super::{BranchOffset, CursorID, Insn, InsnFunction, InsnReference, Program};
use super::{BranchOffset, CursorID, Insn, InsnFunction, InsnReference, JumpTarget, Program};
#[allow(dead_code)]
pub struct ProgramBuilder {
next_free_register: usize,
next_free_cursor_id: usize,
insns: Vec<(Insn, InsnFunction)>,
// for temporarily storing instructions that will be put after Transaction opcode
constant_insns: Vec<(Insn, InsnFunction)>,
// Vector of labels which must be assigned to next emitted instruction
next_insn_labels: Vec<BranchOffset>,
/// Instruction, the function to execute it with, and its original index in the vector.
insns: Vec<(Insn, InsnFunction, usize)>,
/// A span of instructions from (offset_start_inclusive, offset_end_exclusive),
/// that are deemed to be compile-time constant and can be hoisted out of loops
/// so that they get evaluated only once at the start of the program.
pub constant_spans: Vec<(usize, usize)>,
// Cursors that are referenced by the program. Indexed by CursorID.
pub cursor_ref: Vec<(Option<String>, CursorType)>,
/// A vector where index=label number, value=resolved offset. Resolved in build().
label_to_resolved_offset: Vec<Option<InsnReference>>,
label_to_resolved_offset: Vec<Option<(InsnReference, JumpTarget)>>,
// Bitmask of cursors that have emitted a SeekRowid instruction.
seekrowid_emitted_bitmask: u64,
// map of instruction index to manual comment (used in EXPLAIN only)
comments: Option<HashMap<InsnReference, &'static str>>,
comments: Option<Vec<(InsnReference, &'static str)>>,
pub parameters: Parameters,
pub result_columns: Vec<ResultSetColumn>,
pub table_references: Vec<TableReference>,
@ -82,13 +83,12 @@ impl ProgramBuilder {
next_free_register: 1,
next_free_cursor_id: 0,
insns: Vec::with_capacity(opts.approx_num_insns),
next_insn_labels: Vec::with_capacity(2),
cursor_ref: Vec::with_capacity(opts.num_cursors),
constant_insns: Vec::new(),
constant_spans: Vec::new(),
label_to_resolved_offset: Vec::with_capacity(opts.approx_num_labels),
seekrowid_emitted_bitmask: 0,
comments: if opts.query_mode == QueryMode::Explain {
Some(HashMap::new())
Some(Vec::new())
} else {
None
},
@ -98,6 +98,56 @@ impl ProgramBuilder {
}
}
/// Start a new constant span. The next instruction to be emitted will be the first
/// instruction in the span.
pub fn constant_span_start(&mut self) -> usize {
let span = self.constant_spans.len();
let start = self.insns.len();
self.constant_spans.push((start, usize::MAX));
span
}
/// End the current constant span. The last instruction that was emitted is the last
/// instruction in the span.
pub fn constant_span_end(&mut self, span_idx: usize) {
let span = &mut self.constant_spans[span_idx];
if span.1 == usize::MAX {
span.1 = self.insns.len().saturating_sub(1);
}
}
/// End all constant spans that are currently open. This is used to handle edge cases
/// where we think a parent expression is constant, but we decide during the evaluation
/// of one of its children that it is not.
pub fn constant_span_end_all(&mut self) {
for span in self.constant_spans.iter_mut() {
if span.1 == usize::MAX {
span.1 = self.insns.len().saturating_sub(1);
}
}
}
/// Check if there is a constant span that is currently open.
pub fn constant_span_is_open(&self) -> bool {
self.constant_spans
.last()
.map_or(false, |(_, end)| *end == usize::MAX)
}
/// Get the index of the next constant span.
/// Used in [crate::translate::expr::translate_expr_no_constant_opt()] to invalidate
/// all constant spans after the given index.
pub fn constant_spans_next_idx(&self) -> usize {
self.constant_spans.len()
}
/// Invalidate all constant spans after the given index. This is used when we want to
/// be sure that constant optimization is never used for translating a given expression.
/// See [crate::translate::expr::translate_expr_no_constant_opt()] for more details.
pub fn constant_spans_invalidate_after(&mut self, idx: usize) {
self.constant_spans.truncate(idx);
}
pub fn alloc_register(&mut self) -> usize {
let reg = self.next_free_register;
self.next_free_register += 1;
@ -123,12 +173,8 @@ impl ProgramBuilder {
}
pub fn emit_insn(&mut self, insn: Insn) {
for label in self.next_insn_labels.drain(..) {
self.label_to_resolved_offset[label.to_label_value() as usize] =
Some(self.insns.len() as InsnReference);
}
let function = insn.to_function();
self.insns.push((insn, function));
self.insns.push((insn, function, self.insns.len()));
}
pub fn close_cursors(&mut self, cursors: &[CursorID]) {
@ -200,20 +246,69 @@ impl ProgramBuilder {
pub fn add_comment(&mut self, insn_index: BranchOffset, comment: &'static str) {
if let Some(comments) = &mut self.comments {
comments.insert(insn_index.to_offset_int(), comment);
comments.push((insn_index.to_offset_int(), comment));
}
}
// Emit an instruction that will be put at the end of the program (after Transaction statement).
// This is useful for instructions that otherwise will be unnecessarily repeated in a loop.
// Example: In `SELECT * from users where name='John'`, it is unnecessary to set r[1]='John' as we SCAN users table.
// We could simply set it once before the SCAN started.
pub fn mark_last_insn_constant(&mut self) {
self.constant_insns.push(self.insns.pop().unwrap());
if self.constant_span_is_open() {
// no need to mark this insn as constant as the surrounding parent expression is already constant
return;
}
let prev = self.insns.len().saturating_sub(1);
self.constant_spans.push((prev, prev));
}
pub fn emit_constant_insns(&mut self) {
self.insns.append(&mut self.constant_insns);
// move compile-time constant instructions to the end of the program, where they are executed once after Init jumps to it.
// any label_to_resolved_offset that points to an instruction within any moved constant span should be updated to point to the new location.
// the instruction reordering can be done by sorting the insns, so that the ordering is:
// 1. if insn not in any constant span, it stays where it is
// 2. if insn is in a constant span, it is after other insns, except those that are in a later constant span
// 3. within a single constant span the order is preserver
self.insns.sort_by(|(_, _, index_a), (_, _, index_b)| {
let a_span = self
.constant_spans
.iter()
.find(|span| span.0 <= *index_a && span.1 >= *index_a);
let b_span = self
.constant_spans
.iter()
.find(|span| span.0 <= *index_b && span.1 >= *index_b);
if a_span.is_some() && b_span.is_some() {
a_span.unwrap().0.cmp(&b_span.unwrap().0)
} else if a_span.is_some() {
Ordering::Greater
} else if b_span.is_some() {
Ordering::Less
} else {
Ordering::Equal
}
});
for resolved_offset in self.label_to_resolved_offset.iter_mut() {
if let Some((old_offset, target)) = resolved_offset {
let new_offset = self
.insns
.iter()
.position(|(_, _, index)| *old_offset == *index as u32)
.unwrap() as u32;
*resolved_offset = Some((new_offset, *target));
}
}
// Fix comments to refer to new locations
if let Some(comments) = &mut self.comments {
for (old_offset, _) in comments.iter_mut() {
let new_offset = self
.insns
.iter()
.position(|(_, _, index)| *old_offset == *index as u32)
.expect("comment must exist") as u32;
*old_offset = new_offset;
}
}
}
pub fn offset(&self) -> BranchOffset {
@ -226,18 +321,42 @@ impl ProgramBuilder {
BranchOffset::Label(label_n as u32)
}
// Effectively a GOTO <next insn> without the need to emit an explicit GOTO instruction.
// Useful when you know you need to jump to "the next part", but the exact offset is unknowable
// at the time of emitting the instruction.
/// Resolve a label to whatever instruction follows the one that was
/// last emitted.
///
/// Use this when your use case is: "the program should jump to whatever instruction
/// follows the one that was previously emitted", and you don't care exactly
/// which instruction that is. Examples include "the start of a loop", or
/// "after the loop ends".
///
/// It is important to handle those cases this way, because the precise
/// instruction that follows any given instruction might change due to
/// reordering the emitted instructions.
#[inline]
pub fn preassign_label_to_next_insn(&mut self, label: BranchOffset) {
self.next_insn_labels.push(label);
assert!(label.is_label(), "BranchOffset {:?} is not a label", label);
self._resolve_label(label, self.offset().sub(1u32), JumpTarget::AfterThisInsn);
}
/// Resolve a label to exactly the instruction that was last emitted.
///
/// Use this when your use case is: "the program should jump to the exact instruction
/// that was last emitted", and you don't care WHERE exactly that ends up being
/// once the order of the bytecode of the program is finalized. Examples include
/// "jump to the Halt instruction", or "jump to the Next instruction of a loop".
#[inline]
pub fn resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset) {
self._resolve_label(label, to_offset, JumpTarget::ExactlyThisInsn);
}
fn _resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset, target: JumpTarget) {
assert!(matches!(label, BranchOffset::Label(_)));
assert!(matches!(to_offset, BranchOffset::Offset(_)));
self.label_to_resolved_offset[label.to_label_value() as usize] =
Some(to_offset.to_offset_int());
let BranchOffset::Label(label_number) = label else {
unreachable!("Label is not a label");
};
self.label_to_resolved_offset[label_number as usize] =
Some((to_offset.to_offset_int(), target));
}
/// Resolve unresolved labels to a specific offset in the instruction list.
@ -248,19 +367,25 @@ impl ProgramBuilder {
pub fn resolve_labels(&mut self) {
let resolve = |pc: &mut BranchOffset, insn_name: &str| {
if let BranchOffset::Label(label) = pc {
let to_offset = self
.label_to_resolved_offset
.get(*label as usize)
.unwrap_or_else(|| {
panic!("Reference to undefined label in {}: {}", insn_name, label)
});
let Some(Some((to_offset, target))) =
self.label_to_resolved_offset.get(*label as usize)
else {
panic!(
"Reference to undefined or unresolved label in {}: {}",
insn_name, label
);
};
*pc = BranchOffset::Offset(
to_offset
.unwrap_or_else(|| panic!("Unresolved label in {}: {}", insn_name, label)),
+ if *target == JumpTarget::ExactlyThisInsn {
0
} else {
1
},
);
}
};
for (insn, _) in self.insns.iter_mut() {
for (insn, _, _) in self.insns.iter_mut() {
match insn {
Insn::Init { target_pc } => {
resolve(target_pc, "Init");
@ -375,9 +500,10 @@ impl ProgramBuilder {
Insn::InitCoroutine {
yield_reg: _,
jump_on_definition,
start_offset: _,
start_offset,
} => {
resolve(jump_on_definition, "InitCoroutine");
resolve(start_offset, "InitCoroutine");
}
Insn::NotExists {
cursor: _,
@ -470,15 +596,15 @@ impl ProgramBuilder {
change_cnt_on: bool,
) -> Program {
self.resolve_labels();
assert!(
self.constant_insns.is_empty(),
"constant_insns is not empty when build() is called, did you forget to call emit_constant_insns()?"
);
self.parameters.list.dedup();
Program {
max_registers: self.next_free_register,
insns: self.insns,
insns: self
.insns
.into_iter()
.map(|(insn, function, _)| (insn, function))
.collect(),
cursor_ref: self.cursor_ref,
database_header,
comments: self.comments,