From: drh Date: Mon, 31 Aug 2020 16:31:00 +0000 (+0000) Subject: An attempt to improve the performance of the IN-early-out optimization X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=39dde1aa17adb9ec34bff5523d393663926ae05f;p=thirdparty%2Fsqlite.git An attempt to improve the performance of the IN-early-out optimization (see check-in [09fffbdf9f2f6ce3]) by avoiding unnecessary calls to the b-tree search algorithm in OP_IfNoHope when the index key is at hand and the same answer can be obtained by doing a quick key comparison. FossilOrigin-Name: e9d983c6830efdfdb734409465f071f38acf3050571aadb9b05b885328baf77c --- diff --git a/manifest b/manifest index 3960de1cb1..c37268932b 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sthe\sdocumentation\sfor\sthe\sOP_IdxGT\sfamily\sof\sopcodes\sto\sshow\sthat\sthe\nP5\soperand\sis\snot\sused. -D 2020-08-31T12:29:03.480 +C An\sattempt\sto\simprove\sthe\sperformance\sof\sthe\sIN-early-out\soptimization\n(see\scheck-in\s[09fffbdf9f2f6ce3])\sby\savoiding\sunnecessary\scalls\sto\sthe\nb-tree\ssearch\salgorithm\sin\sOP_IfNoHope\swhen\sthe\sindex\skey\sis\sat\shand\sand\nthe\ssame\sanswer\scan\sbe\sobtained\sby\sdoing\sa\squick\skey\scomparison. +D 2020-08-31T16:31:00.075 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -607,9 +607,9 @@ F src/upsert.c 2920de71b20f04fe25eb00b655d086f0ba60ea133c59d7fa3325c49838818e78 F src/utf.c ee39565f0843775cc2c81135751ddd93eceb91a673ea2c57f61c76f288b041a0 F src/util.c c0c7977de7ef9b8cb10f6c85f2d0557889a658f817b0455909a49179ba4c8002 F src/vacuum.c 492422c1463c076473bae1858799c7a0a5fe87a133d1223239447c422cd26286 -F src/vdbe.c c5da1456c9de0993055be9c10ebc5f5eb2be75d28cb01c8abc2f083923835a2d +F src/vdbe.c a0ce31a5dc3dd5a6933fb616e1d1fcafb2a09143580b729b8346777227e5231a F src/vdbe.h 83603854bfa5851af601fc0947671eb260f4363e62e960e8a994fb9bbcd2aaa1 -F src/vdbeInt.h 762abffb7709f19c2cb74af1bba73a900f762e64f80d69c31c9ae89ed1066b60 +F src/vdbeInt.h 43341faf09fb620acc962be62ae339e7b77715207862e2b2e596f7f2f39e3828 F src/vdbeapi.c c5e7cb2ab89a24d7f723e87b508f21bfb1359a04db5277d8a99fd1e015c12eb9 F src/vdbeaux.c b39d2e0e7126cd4629874dd7b67162b9f0d200b620d2b4c16d400949a2f1094b F src/vdbeblob.c 253ed82894924c362a7fa3079551d3554cd1cdace39aa833da77d3bc67e7c1b1 @@ -624,7 +624,7 @@ F src/wal.h c3aa7825bfa2fe0d85bef2db94655f99870a285778baa36307c0a16da32b226a F src/walker.c 3df26a33dc4f54e8771600fb7fdebe1ece0896c2ad68c30ab40b017aa4395049 F src/where.c 23f47e845e304a41d0b221bf67bd170014ae08b673076813fcd945dda1a3d4af F src/whereInt.h eb8c2847fb464728533777efec1682b3c074224293b2da73513c61a609efbeab -F src/wherecode.c 110fa357bf453e0d30bf5ebb2cc86ea34a3631c39b857f30c228fd325cb53ae7 +F src/wherecode.c 28b243fe624a0f111c7680eb0fe6e9c23b6b0a5b35f9a0f75e87ae1534b7615a F src/whereexpr.c 264d58971eaf8256eb5b0917bcd7fc7a1f1109fdda183a8382308a1b18a2dce7 F src/window.c edd6f5e25a1e8f2b6f5305b7f5f7da7bb35f07f0d432b255b1d4c2fcab4205aa F test/8_3_names.test ebbb5cd36741350040fd28b432ceadf495be25b2 @@ -1879,7 +1879,10 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P ded1a75b3cf39834d38a385f38ae969b296f6c9409856b7eea08645e861b1ac2 -R d9747280ab6c456500686b04f563aa50 +P 62f7d2a61259f296ffdcb3b3ee1a13925c4563ac8ed669f8a8a63fc7bc3a0a37 +R fe444095b3e78d9325a2970b51b1a43d +T *branch * in-early-out +T *sym-in-early-out * +T -sym-trunk * U drh -Z 96026921b32a7e290f8ba5e4e39f8431 +Z effb995a21bb3b32c1f5d827a8cd2f01 diff --git a/manifest.uuid b/manifest.uuid index 417012c05c..1be0ec858d 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -62f7d2a61259f296ffdcb3b3ee1a13925c4563ac8ed669f8a8a63fc7bc3a0a37 \ No newline at end of file +e9d983c6830efdfdb734409465f071f38acf3050571aadb9b05b885328baf77c \ No newline at end of file diff --git a/src/vdbe.c b/src/vdbe.c index bc5addabab..63033986cf 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -137,6 +137,54 @@ static void test_trace_breakpoint(int pc, Op *pOp, Vdbe *v){ } #endif + +#ifdef SQLITE_DEBUG +/* This is a validation routine that occurs only inside of assert() +** statements. Its purpose is to verify that the generated bytecode +** is correct. It only runs on debugging builds. +** +** Verify that the pOp opcode is an OP_SeekGE or OP_SeekLE that is +** followed by a corresponding OP_IdxGT or OP_IdxLT, possibly with +** an intervening OP_SeekHit. Return 1 if the constraint is true. +** Return 0 if something unexpected is seen. +*/ +static int seekFollowedByPairedIdxCompare(VdbeOp *pOp){ + VdbeOp *pNext; + + /* Find the subsequent opcode. We are allowed to skip over a single + ** OP_SeekHit against the same cursor with a P2 value of 1. + */ + pNext = pOp + 1; + if( pNext->opcode==OP_SeekHit ){ + if( pNext->p2!=1 ) return 0; + if( pNext->p1!=pOp->p1 ) return 0; + pNext++; + } + + /* This routine verifies that the opcodes are correct for a + ** BTREE_SEEK_EQ lookup. So the two opcodes involved must be either: + ** + ** * OP_SeekGE followed by OP_IdxGT + ** * OP_SeekLE followed by OP_IdxLT + */ + if( pOp->opcode==OP_SeekGE ){ + if( pNext->opcode!=OP_IdxGT ) return 0; + }else if( pOp->opcode==OP_SeekLE ){ + if( pNext->opcode!=OP_IdxLT ) return 0; + }else{ + return 0; + } + + /* The OP_SeekXX and OP_IdxXX must have the same arguments. + */ + if( pOp->p1!=pNext->p1 ) return 0; + if( pOp->p2!=pNext->p2 ) return 0; + if( pOp->p3!=pNext->p3 ) return 0; + if( pOp->p4.i!=pNext->p4.i ) return 0; + return 1; +} +#endif /* SQLITE_DEBUG */ + /* ** Invoke the VDBE coverage callback, if that callback is defined. This ** feature is used for test suite validation only and does not appear an @@ -4286,18 +4334,12 @@ case OP_SeekGT: { /* jump, in3, group */ /* For a cursor with the OPFLAG_SEEKEQ/BTREE_SEEK_EQ hint, only the ** OP_SeekGE and OP_SeekLE opcodes are allowed, and these must be ** immediately followed by an OP_IdxGT or OP_IdxLT opcode, respectively, - ** with the same key. + ** with the same key. There might be a single OP_SeekHit opcode in + ** between the OP_SeekXX and OP_IdxXX. */ if( sqlite3BtreeCursorHasHint(pC->uc.pCursor, BTREE_SEEK_EQ) ){ - eqOnly = 1; - assert( pOp->opcode==OP_SeekGE || pOp->opcode==OP_SeekLE ); - assert( pOp[1].opcode==OP_IdxLT || pOp[1].opcode==OP_IdxGT ); - assert( pOp->opcode==OP_SeekGE || pOp[1].opcode==OP_IdxLT ); - assert( pOp->opcode==OP_SeekLE || pOp[1].opcode==OP_IdxGT ); - assert( pOp[1].p1==pOp[0].p1 ); - assert( pOp[1].p2==pOp[0].p2 ); - assert( pOp[1].p3==pOp[0].p3 ); - assert( pOp[1].p4.i==pOp[0].p4.i ); + eqOnly = 1 + (pOp[1].opcode==OP_SeekHit); + assert( seekFollowedByPairedIdxCompare(pOp) ); } nField = pOp->p4.i; @@ -4377,8 +4419,8 @@ seek_not_found: if( res ){ goto jump_to_p2; }else if( eqOnly ){ - assert( pOp[1].opcode==OP_IdxLT || pOp[1].opcode==OP_IdxGT ); - pOp++; /* Skip the OP_IdxLt or OP_IdxGT that follows */ + assert( pOp[eqOnly].opcode==OP_IdxLT || pOp[eqOnly].opcode==OP_IdxGT ); + pOp += eqOnly; /* Skip the OP_IdxLt or OP_IdxGT that follows */ } break; } @@ -4389,16 +4431,16 @@ seek_not_found: ** Set the seekHit flag on cursor P1 to the value in P2. ** The seekHit flag is used by the IfNoHope opcode. ** -** P1 must be a valid b-tree cursor. P2 must be a boolean value, -** either 0 or 1. +** P1 must be a valid b-tree cursor. P2 must be an integer +** between 0 and 3. */ case OP_SeekHit: { VdbeCursor *pC; assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - assert( pOp->p2==0 || pOp->p2==1 ); - pC->seekHit = pOp->p2 & 1; + assert( pOp->p2>=0 && pOp->p2<=3 ); + pC->seekHit = pOp->p2 & 3; break; } @@ -4452,33 +4494,6 @@ case OP_IfNotOpen: { /* jump */ ** ** See also: Found, NotExists, NoConflict, IfNoHope */ -/* Opcode: IfNoHope P1 P2 P3 P4 * -** Synopsis: key=r[P3@P4] -** -** Register P3 is the first of P4 registers that form an unpacked -** record. -** -** Cursor P1 is on an index btree. If the seekHit flag is set on P1, then -** this opcode is a no-op. But if the seekHit flag of P1 is clear, then -** check to see if there is any entry in P1 that matches the -** prefix identified by P3 and P4. If no entry matches the prefix, -** jump to P2. Otherwise fall through. -** -** This opcode behaves like OP_NotFound if the seekHit -** flag is clear and it behaves like OP_Noop if the seekHit flag is set. -** -** This opcode is used in IN clause processing for a multi-column key. -** If an IN clause is attached to an element of the key other than the -** left-most element, and if there are no matches on the most recent -** seek over the whole key, then it might be that one of the key element -** to the left is prohibiting a match, and hence there is "no hope" of -** any match regardless of how many IN clause elements are checked. -** In such a case, we abandon the IN clause search early, using this -** opcode. The opcode name comes from the fact that the -** jump is taken if there is "no hope" of achieving a match. -** -** See also: NotFound, SeekHit -*/ /* Opcode: NoConflict P1 P2 P3 P4 * ** Synopsis: key=r[P3@P4] ** @@ -4502,15 +4517,6 @@ case OP_IfNotOpen: { /* jump */ ** ** See also: NotFound, Found, NotExists */ -case OP_IfNoHope: { /* jump, in3 */ - VdbeCursor *pC; - assert( pOp->p1>=0 && pOp->p1nCursor ); - pC = p->apCsr[pOp->p1]; - assert( pC!=0 ); - if( pC->seekHit ) break; - /* Fall through into OP_NotFound */ - /* no break */ deliberate_fall_through -} case OP_NoConflict: /* jump, in3 */ case OP_NotFound: /* jump, in3 */ case OP_Found: { /* jump, in3 */ @@ -4527,6 +4533,7 @@ case OP_Found: { /* jump, in3 */ if( pOp->opcode!=OP_NoConflict ) sqlite3_found_count++; #endif +vdbe_op_notfound: /* OP_IfNoHope jumps here, sometimes */ assert( pOp->p1>=0 && pOp->p1nCursor ); assert( pOp->p4type==P4_INT32 ); pC = p->apCsr[pOp->p1]; @@ -5865,14 +5872,15 @@ case OP_FinishSeek: { ** If the P1 index entry is less than or equal to the key value then jump ** to P2. Otherwise fall through to the next instruction. */ -case OP_IdxLE: /* jump */ -case OP_IdxGT: /* jump */ -case OP_IdxLT: /* jump */ -case OP_IdxGE: { /* jump */ +case OP_IdxLE: /* jump, group */ +case OP_IdxGT: /* jump, group */ +case OP_IdxLT: /* jump, group */ +case OP_IdxGE: { /* jump, group */ VdbeCursor *pC; int res; UnpackedRecord r; +vdbe_op_idxne: /* Virtual OP_IdxNE opcode. See OP_IfNoHope */ assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); @@ -5887,7 +5895,8 @@ case OP_IdxGE: { /* jump */ assert( pOp->opcode==OP_IdxLE || pOp->opcode==OP_IdxGT ); r.default_rc = -1; }else{ - assert( pOp->opcode==OP_IdxGE || pOp->opcode==OP_IdxLT ); + assert( pOp->opcode==OP_IdxGE || pOp->opcode==OP_IdxLT + || pOp->opcode==OP_IfNoHope ); r.default_rc = 0; } r.aMem = &aMem[pOp->p3]; @@ -5904,7 +5913,8 @@ case OP_IdxGE: { /* jump */ rc = sqlite3VdbeIdxKeyCompare(db, pC, &r, &res); assert( (OP_IdxLE&1)==(OP_IdxLT&1) && (OP_IdxGE&1)==(OP_IdxGT&1) ); if( (pOp->opcode&1)==(OP_IdxLT&1) ){ - assert( pOp->opcode==OP_IdxLE || pOp->opcode==OP_IdxLT ); + assert( pOp->opcode==OP_IdxLE || pOp->opcode==OP_IdxLT + || pOp->opcode==OP_IfNoHope ); res = -res; }else{ assert( pOp->opcode==OP_IdxGE || pOp->opcode==OP_IdxGT ); @@ -5916,6 +5926,71 @@ case OP_IdxGE: { /* jump */ break; } +/* Opcode: IfNoHope P1 P2 P3 P4 * +** Synopsis: key=r[P3@P4] +** +** Register P3 is the first of P4 registers that form an unpacked +** record. P1 is number of an index-btree cursor. P2 is a jump +** destination. So, in other words, the operands to this opcode +** are the same as the operands to OP_NotFound and OP_IdxGT. +** +** The behavior of this opcode depends on the current seekFlag setting. +** (See the OP_SeekHit.) +** +**
    +**
  • seekHit==0 → OP_NotFound +**
  • seekHit==1 → OP_IdxGT or OP_IdxLT +**
  • seekHit==2 → OP_Noop +**
+** +** Actually, when seekHit==1, this routine behaves as if it were an +** OP_IdxNE opcode. It has all the same parameters as OP_IdxGT and +** OP_IdxLT, but the jump is taken if the key is not equal to the index +** record, rather than if the key is greater than or less than the +** index record, respectively. +** +** This opcode is used in IN clause processing for a multi-column key. +** If an IN clause is attached to an element of the key other than the +** left-most element, and if there are no matches on the most recent +** seek over the whole key, then it might be that one of the key element +** to the left is prohibiting a match, and hence there is "no hope" of +** any match regardless of how many IN clause elements are checked. +** In such a case, we abandon the IN clause search early, using this +** opcode. The opcode name comes from the fact that the +** jump is taken if there is "no hope" of achieving a match. +** +** This opcode is an optimization. It can always behave as OP_Noop +** and the correct result will still be obtained, though perhaps not +** quite as quickly. +** +** See also: NotFound, SeekHit, IdxGT +*/ +case OP_IfNoHope: { /* jump, in3, group */ + VdbeCursor *pC; + assert( pOp->p1>=0 && pOp->p1nCursor ); + pC = p->apCsr[pOp->p1]; + assert( pC!=0 ); + if( pC->seekHit>=2 ){ + /* There has been one or more successful OP_IdxXX opcodes ("successful" + ** in the sense that the jump was not taken because the key and index + ** matched). So a match is possible. We do not want to abort. Fall + ** through to the next opcode without doing anything. */ + break; + }else if( pC->seekHit==1 ){ + /* The initial OP_SeekXX opcode was successful (in that it found a + ** candidate row) but all subsequent OP_IdxXX opcodes failed. The + ** index should have been left pointing at the candidate row. In this + ** case, this opcode works like a "OP_IdxNE" opcode - similar to + ** OP_IdxGT but instead of jumping if the index entry is greater than + ** the key, it jumps if the index entry is not equal to the key. + */ + goto vdbe_op_idxne; + } + /* The initial OP_SeekXX opcode took its jump, meaning that it found + ** no candidate rows. Treat this opcode as if it were an OP_NotFound */ + goto vdbe_op_notfound; +} + /* Opcode: Destroy P1 P2 P3 * * ** ** Delete an entire database table or index whose root page in the database diff --git a/src/vdbeInt.h b/src/vdbeInt.h index 901569742f..3c0dccb0cf 100644 --- a/src/vdbeInt.h +++ b/src/vdbeInt.h @@ -86,7 +86,7 @@ struct VdbeCursor { Bool isEphemeral:1; /* True for an ephemeral table */ Bool useRandomRowid:1; /* Generate new record numbers semi-randomly */ Bool isOrdered:1; /* True if the table is not BTREE_UNORDERED */ - Bool seekHit:1; /* See the OP_SeekHit and OP_IfNoHope opcodes */ + unsigned seekHit:2; /* See the OP_SeekHit and OP_IfNoHope opcodes */ Btree *pBtx; /* Separate file holding temporary table */ i64 seqCount; /* Sequence counter */ u32 *aAltMap; /* Mapping from table to index column numbers */ diff --git a/src/wherecode.c b/src/wherecode.c index 145b6fa5f5..f17cda94e6 100644 --- a/src/wherecode.c +++ b/src/wherecode.c @@ -1870,6 +1870,9 @@ Bitmask sqlite3WhereCodeOneLoopStart( /* Check if the index cursor is past the end of the range. */ if( nConstraint ){ + if( pLoop->wsFlags & WHERE_IN_EARLYOUT ){ + sqlite3VdbeAddOp2(v, OP_SeekHit, iIdxCur, 1); + } if( regBignull ){ /* Except, skip the end-of-range check while doing the NULL-scan */ sqlite3VdbeAddOp2(v, OP_IfNot, regBignull, sqlite3VdbeCurrentAddr(v)+3); @@ -1902,7 +1905,7 @@ Bitmask sqlite3WhereCodeOneLoopStart( } if( pLoop->wsFlags & WHERE_IN_EARLYOUT ){ - sqlite3VdbeAddOp2(v, OP_SeekHit, iIdxCur, 1); + sqlite3VdbeAddOp2(v, OP_SeekHit, iIdxCur, 2); } /* Seek the table cursor, if required */