Project

General

Profile

Bug #9756

Performance improvements for CONTAINS udf related methods.

Added by Stefanel Pezamosca over 1 year ago. Updated over 1 year ago.

Status:
Test
Priority:
Normal
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
production:
No
env_name:
topics:

start.p Magnifier (1.23 KB) Stefanel Pezamosca, 03/11/2025 06:08 AM

History

#1 Updated by Stefanel Pezamosca over 1 year ago

  • File start.pMagnifier added
  • Status changed from New to WIP
  • % Done changed from 0 to 100

Working at #7020 I also made some performance improvements for CONTAINS udf related methods. LogicalExpressionConverter.Lexer, Contains and FqlToSqlConverter. I decided to extract these optimizations in another branch so they can reach trunk faster.
So, I created 9756a from trunk revision 15764 and committed the changes in revision 15765. There are also some javadoc fixes.

I attached the code that I used to test this.

100000 iterations each.

Without optimisations                   -> 4600ms - 4800ms
With LEC.Lexer optimisations            -> 4500ms - 4600ms
With LEC.Lexer + Contains optimisations -> 3600ms - 3800ms
So ~20% improvement.

Biggest bottleneck is FQL parsing (FQLParser). Adding a cache for already parsed fql ASTs shows great performance improvement for this specific scenario.

Above optimisations + FQL AST caching   -> 2300ms - 2400ms
So another ~35% improvement.

There is a total of ~50% improvement for this scenario with the 9756a changes.

#2 Updated by Stefanel Pezamosca over 1 year ago

  • Status changed from WIP to Review

Please review!

#3 Updated by Stefanel Pezamosca over 1 year ago

  • Assignee set to Stefanel Pezamosca

#4 Updated by Stefanel Pezamosca over 1 year ago

  • reviewer Alexandru Lungu, Ovidiu Maxiniuc added

#5 Updated by Ovidiu Maxiniuc over 1 year ago

Review of 9756a/15766.

I agree that these are two directions we can improve performance. The ideas are good. But I think there are a few issues to discuss:
  • in FqlToSqlConverter, the fqlAstCache is static, and the key is just the fql. This is not correct. The current solution will work only on simple cases. Think of two queries on DIFFERENT databases (maybe different buffers of same database?) which apparently request apparently the same data from SQL, but the table structure are different. The solution is to create a key for fqlAstCache which contains the DMO classes as well. Their paths contain enough information to distinct tables from different databases, even if they have the same name;
  • also in FqlToSqlConverter.fqlAstCache, I think there are 90% cases when the AST tree does not require to be duplicate() -d when added to cache or extracted. That is because the FQLAst root is usually only processed locally, in toSQL() and does not 'leak'. For cases when the instance is created and preprocess() -ed, we can add an annotation to mark 'mutable' trees in case they are affected by processStatement(). I think this happens only for word-indexed instances in case of SELECT statements. This is just an assumption and need to be double checked;
  • related to the new, rather large, boolean arrays in Contains and LogicalExpressionConverter. I think we can replace these occurrences with BitSet. These are pretty fast data structures and use the memory optimally (1 bit per boolean), in a dynamic manner (allocating memory only for set places);
  • in Functions.getDelimiterPattern() and LogicalExpressionConverter$Lexer(), the compiled patterns are simplified. Also in the latter case skipPatterns is static and NOT synchronised! At least not evidently. I looked up the call stack a few levels and I couldn’t spot any.

#6 Updated by Stefanel Pezamosca over 1 year ago

Ovidiu Maxiniuc wrote:

Review of 9756a/15766.

I agree that these are two directions we can improve performance. The ideas are good. But I think there are a few issues to discuss:
  • in FqlToSqlConverter, the fqlAstCache is static, and the key is just the fql. This is not correct. The current solution will work only on simple cases. Think of two queries on DIFFERENT databases (maybe different buffers of same database?) which apparently request apparently the same data from SQL, but the table structure are different. The solution is to create a key for fqlAstCache which contains the DMO classes as well. Their paths contain enough information to distinct tables from different databases, even if they have the same name;

The cache I made only saves the ast given by FQLParser, and has only the String fql as argument. So, without any other processing.

#7 Updated by Stefanel Pezamosca over 1 year ago

Ovidiu Maxiniuc wrote:

  • also in FqlToSqlConverter.fqlAstCache, I think there are 90% cases when the AST tree does not require to be duplicate() -d when added to cache or extracted. That is because the FQLAst root is usually only processed locally, in toSQL() and does not 'leak'. For cases when the instance is created and preprocess() -ed, we can add an annotation to mark 'mutable' trees in case they are affected by processStatement(). I think this happens only for word-indexed instances in case of SELECT statements. This is just an assumption and need to be double checked;

Hmm, I think that the cache only needs to be made for queries with CONTAINS because for these queryWasRewritten would be true and because of this the cache used in Query.createSqlQuery is not used.

#8 Updated by Stefanel Pezamosca over 1 year ago

Ovidiu Maxiniuc wrote:

  • related to the new, rather large, boolean arrays in Contains and LogicalExpressionConverter. I think we can replace these occurrences with BitSet. These are pretty fast data structures and use the memory optimally (1 bit per boolean), in a dynamic manner (allocating memory only for set places);

boolean[] is fastest with some memory compromise indeed. I will also test with BitSet.

  • in Functions.getDelimiterPattern() and LogicalExpressionConverter$Lexer(), the compiled patterns are simplified. Also in the latter case skipPatterns is static and NOT synchronised! At least not evidently. I looked up the call stack a few levels and I couldn’t spot any.

Oh, right skipPatterns should have been a ConcurrentHashMap.

Do you have anything else to add ?

#9 Updated by Ovidiu Maxiniuc over 1 year ago

Stefanel Pezamosca wrote:

The cache I made only saves the ast given by FQLParser, and has only the String fql as argument. So, without any other processing.

That's right. I've been deceived when comparing the revisions. Scratch the first point. And seconds, as well. Unless we extend the cache to processed trees.

boolean[] is fastest with some memory compromise indeed. I will also test with BitSet.

That's true. But BitSet is optimised for accesses, so I think the trade is worth it.

  • in Functions.getDelimiterPattern() and LogicalExpressionConverter$Lexer(), the compiled patterns are simplified. Also in the latter case skipPatterns is static and NOT synchronised! At least not evidently. I looked up the call stack a few levels and I couldn’t spot any.

Oh, right skipPatterns should have been a ConcurrentHashMap.

Correct. I was thinking of external synchronisation. Switching to ConcurrentHashMap should do it.

Do you have anything else to add ?

Not at this time :).

#10 Updated by Stefanel Pezamosca over 1 year ago

I changed boolean[] to BitSet and used ConcurrentHashMap for skipPatterns in 9756a revision 15767.

#11 Updated by Ovidiu Maxiniuc over 1 year ago

Cool. I have looked at the code. The changes are minimal. They are OK in my opinion. However, we can assume the first 128 characters (base Latin set) have high probabilities to be processed so we can pre-allocate them by using new BitSet(128).

At the same time, please redo the original 100000-iterations test from the 1st note and post the results.

#12 Updated by Stefanel Pezamosca over 1 year ago

  • reviewer deleted (Alexandru Lungu)

Ovidiu Maxiniuc wrote:

Cool. I have looked at the code. The changes are minimal. They are OK in my opinion. However, we can assume the first 128 characters (base Latin set) have high probabilities to be processed so we can pre-allocate them by using new BitSet(128).

At the same time, please redo the original 100000-iterations test from the 1st note and post the results.

Done. New results with updated baseline. After multiple executions, the values observed were approximately within the following range:

Without 9756a: -> 5200ms - 5600ms
With 9756a:    -> 2600ms - 2800ms

#13 Updated by Stefanel Pezamosca over 1 year ago

  • Status changed from Review to Internal Test

I will continue testing the functionality with different parameters for contains.

#14 Updated by Stefanel Pezamosca over 1 year ago

Oh, I discovered an issue that is also in trunk. One example of problematic statement contains(words, -) I will fix this in 9756a.

#15 Updated by Stefanel Pezamosca over 1 year ago

I fixed this in 9756a revision 15769.

Other tests passed. If there are no other objections I think 9756a is safe for trunk.

#16 Updated by Stefanel Pezamosca over 1 year ago

  • Status changed from Internal Test to Review

I made some other minor changes in revision 15769.

#17 Updated by Ovidiu Maxiniuc over 1 year ago

  • Status changed from Review to Internal Test

O am OK with the changes in r15769.

#18 Updated by Stefanel Pezamosca over 1 year ago

I have continued testing various CONTAINS testcases and there were no other issues.
This can be merged.

#19 Updated by Alexandru Lungu over 1 year ago

  • Status changed from Internal Test to Merge Pending

Please merge 9756a to trunk after 9676a.

#20 Updated by Stefanel Pezamosca over 1 year ago

Branch 9756a was merged into trunk as rev. 15784 and archived.

#21 Updated by Stefanel Pezamosca over 1 year ago

  • Status changed from Merge Pending to Test

Also available in: Atom PDF