Skip to content

Instantly share code, notes, and snippets.

@sebres
Last active January 31, 2025 23:46
Show Gist options
  • Save sebres/dc4efcf55fdb349792220e43a7261bcd to your computer and use it in GitHub Desktop.
Save sebres/dc4efcf55fdb349792220e43a7261bcd to your computer and use it in GitHub Desktop.
sqlite performance of simplest selects or attempt to explain strange unexpected overhead comparing to native tcl

SQLite performance of simplest selects or attempt to explain strange unexpected overhead comparing to native tcl

... using time measurement only, estimating the bottleneck by comparision with different tcl approaches and internals.

Initialize

# command to measure within a proc (better compiled and fewer overhead for access vars, don't leak vars, etc):
proc timeit args {
  set v 10;
  if {[lindex $args 0] eq "-setup"} {set args [lassign $args _ c]; eval $c};
  timerate {*}$args
}
timeit -calibrate {}
package require sqlite3
sqlite3 db :memory:

1st test - get single variable in tcl and sqlite:

% timeit { set v }
0.000929 µs/# 23378303 # 1076547384 #/sec 21.716 net-ms
% timeit { db eval { select :v } }
0.691755 µs/# 1363139 # 1445598 #/sec 942.958 net-ms

Hmm... it has very large deviation. But lets estimate the overhead between compiled and non-compiled eval (because set v will be compiled in TEBC very efficiently compared to non-compiled command):

Lets rewrite loadScalar instruction to push + loadStk combi:

1st test (variant 2):

% timeit { set [list v] }
0.010497 µs/# 19104745 # 95263655 #/sec 200.546 net-ms
% timeit { db eval { select :v } }
0.693341 µs/# 1360200 # 1442292 #/sec 943.082 net-ms

Nevertheless, blatant difference. We need possibly rewrite it to involve the overhead for the command invocation (TEBCResume, NRE trampoline, etc)

Lets try to rewrite it using invokeStk, or simply invokeExpanded (what is comparable in eval times):

1st test (variant 3):

% timeit -setup { set cmd set } { $cmd v }; # invokeStk
0.148422 µs/# 5255760 # 6737566 #/sec 780.068 net-ms
% timeit { {*}[list set v] }; # invokeExpanded
0.158082 µs/# 5001789 # 6325813 #/sec 790.695 net-ms
% timeit { db eval { select :v } }
0.692798 µs/# 1361203 # 1443421 #/sec 943.039 net-ms

Not so large anymore, but lets estimate the overhead for the prepare + statement compilation, caching etc. So simply test multiple variables instead of single variable.

2nd test - multiple vars involved:

% timeit -setup { set cmd list; set c [list v] } { $cmd [set $c] [set $c] [set $c] [set $c] [set $c] [set $c] [set $c] [set $c] [set $c] [set $c] }
0.376549 µs/# 2390087 # 2655699 #/sec 899.984 net-ms
% timeit { db eval {select :v, :v, :v, :v, :v, :v, :v, :v, :v, :v} }
2.969883 µs/# 332035 # 336713 #/sec 986.105 net-ms

Oops! It grows unexpectedly, may be it is the flexible typing or type affinity feature of sqlite. Lets check it with fixed string or null (variable is not there):

3rd test - check constant string and variable null:

% timeit                    { db eval {select '', '', '', '', '', '', '', '', '', ''} }
0.944053 µs/# 1014303 # 1059262 #/sec 957.556 net-ms
% timeit -setup { unset v } { db eval {select :v, :v, :v, :v, :v, :v, :v, :v, :v, :v} }
1.409812 µs/# 688867 # 709314 #/sec 971.173 net-ms
% timeit                    { db eval {select :v, :v, :v, :v, :v, :v, :v, :v, :v, :v} }
3.081652 µs/# 320153 # 324501 #/sec 986.600 net-ms

OK, here maybe a potential for an optimization... But lets check, whether it is by constructing of the result, for instance using multiple reads but single value to return:

4th test - 10 vars, 1 result column, eval vs. onecolumn:

# static 10 fields, eval vs. onecolumn:
% timeit { db eval      {select '' , '' , '' , '' , '' , '' , '' , '' , '' , ''} }
0.974208 µs/# 984258 # 1026474 #/sec 958.872 net-ms
% timeit { db onecolumn {select '' , '' , '' , '' , '' , '' , '' , '' , '' , ''} }
0.305482 µs/# 2879118 # 3273510 #/sec 879.520 net-ms
# static 1 fields (expr), eval vs. onecolumn:
% timeit { db eval      {select '' + '' + '' + '' + '' + '' + '' + '' + '' + ''} }
0.843663 µs/# 1129294 # 1185308 #/sec 952.743 net-ms
% timeit { db onecolumn {select '' + '' + '' + '' + '' + '' + '' + '' + '' + ''} }
0.716921 µs/# 1317929 # 1394853 #/sec 944.851 net-ms
# dynamic 10 vars => 10 fields, eval vs. onecolumn:
% timeit { db eval      {select :v , :v , :v , :v , :v , :v , :v , :v , :v , :v} }
2.942984 µs/# 335027 # 339791 #/sec 985.979 net-ms
% timeit { db onecolumn {select :v , :v , :v , :v , :v , :v , :v , :v , :v , :v} }
2.071647 µs/# 473150 # 482707 #/sec 980.200 net-ms
# dynamic 10 vars => 1 fields (expr), eval vs. onecolumn:
% timeit { db eval      {select :v + :v + :v + :v + :v + :v + :v + :v + :v + :v} }
0.987323 µs/# 971658 # 1012840 #/sec 959.340 net-ms
% timeit { db onecolumn {select :v + :v + :v + :v + :v + :v + :v + :v + :v + :v} }
0.887403 µs/# 1076138 # 1126883 #/sec 954.968 net-ms

Yes, the build of result set seems to be involved too indeed. But it's thought too simple...

Bottom line

How one can see (especially considering difference by 1st test) - it has huge potential to improve by:

  • better compilation or caching of statement and smarter prepare of set of parameters (vars)
  • the variable binding (especially deterministic, when variable is a constant)
  • integration sqlite-handle command within TEBC
  • more efficient resultset creation or resultset prepare (preserve list by known columns and result lengh, better type affinity of tcl-object, etc)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment