Skip to content

GH-40024: [C++][Gandiva] Constructing LLVM module with only necessary functions - #40031

Open
niyue wants to merge 9 commits into
apache:mainfrom
niyue:feature/gdv-engine-perf
Open

GH-40024: [C++][Gandiva] Constructing LLVM module with only necessary functions#40031
niyue wants to merge 9 commits into
apache:mainfrom
niyue:feature/gdv-engine-perf

Conversation

@niyue

@niyueniyue commented Feb 11, 2024

Copy link
Copy Markdown
Contributor

Rationale for this change

This PR tries to address GH-40024. It keeps track of used functions in Gandiva expressions, and uses that information to avoid defining unused C functions in LLVM module, and avoid loading/linking the LLVM bitcode if no LLVM IR function is used in the expressions. And this helps expression compilation performance.

What changes are included in this PR?

  • ExprDecomposer has a new member called used_functions_ to keep track of used functions after visiting the expressions.
  • Engine's Init process is postponed to after all expressions are decomposed, so that all functions used can be obtained before constructing LLVM modules.
  • AddGlobalMappingForFunc only for used functions
  • Separate LLVM bitcode (irhelpers.bc) into two parts, one for mandatory bitcode, and the other for optional bitcode.
  • Load bitcode only when necessary

Are these changes tested?

  • Yes, several unit tests are added to cover these changes.
  • And several micro benchmarks are added to verify the performance change.

Are there any user-facing changes?

No

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #40024has been automatically assigned in GitHub to PR creator.

Comment threadcpp/src/gandiva/CMakeLists.txt Outdated

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two bitcode files now, the original irhelpers.bc is optionally used, and the mandatory_ir.bc is mandatory and always linked to the LLVM module constructed.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 11, 2024
Comment threadcpp/src/gandiva/engine.cc Outdated

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor change, after profiling, I found constructing TargetMachine takes some time, and here I tries to re-use the TargetMachine instance created previously.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, all the mandatory IR functions, including "bitMapGetBit"/"bitMapSetBit"/"bitMapValidityGetBit"/"bitMapClearBitIfFalse", are defined in bitmap.cc file

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new use_cache parameter is added to the benchmark, when use_cache, the expression is the same across iterations (a constant 1 is used), so that code cache will take effect during multiple times of projector construction.

@niyue
niyueforce-pushed the feature/gdv-engine-perf branch from 1c61cf0 to 63f5a24CompareFebruary 11, 2024 08:29
Comment threadcpp/src/gandiva/engine.h Outdated

@niyueniyueFeb 11, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two reasons I make this change for this factory method:

  • I realize GandivaObjectCache is a higher level concept, to construct it, ExpressionCacheKey needs to be used, and the caller needs to be aware of concepts like Schema/Configuration/Expression, and Engine is lower level and is not aware of these concepts at all. So I switch to use a lower level API (llvm::ObjectCache) instead
  • in this PR, I tries to avoid constructing too many TargetMachine for better performance, a TargetIRAnalysis is passed to Engine's constructor (the Engine only uses TargetIRAnalysis during optimizing code, but doesn't use TargetMachine directly). For some unknown reason (it seems to be a bug of LLVM for me), the TargetIRAnalysis obtained from a TargetMachine requires the TargetMachine instance to be alive when running optimization (OptimizeModuleWithNewPassManager), so I change this object_cache to have a default value if nullptr is provided (a cache that does not cache at all), and the only place that constructs Engine without object_cache were several unit tests in engine_llvm_test.cc and llvm_generator_test.cc, and the corresponding target_machine will be moved into TMOwningSimpleCompiler and be alive:
 jit_builder.setCompileFunctionCreator(
[&object_cache, &target_machine](llvm::orc::JITTargetMachineBuilder JTMB)
-> llvm::Expected<std::unique_ptr<llvm::orc::IRCompileLayer::IRCompiler>> {
// after compilation, the object code will be stored into the given object
// cache
return std::make_unique<llvm::orc::TMOwningSimpleCompiler>(
std::move(target_machine), object_cache);
});

@niyue
niyueforce-pushed the feature/gdv-engine-perf branch 4 times, most recently from 686eb72 to 6c63093CompareFebruary 12, 2024 04:57
Comment threadcpp/src/gandiva/engine.h Outdated
@niyue

niyue commented Feb 12, 2024

Copy link
Copy Markdown
ContributorAuthor

I added several micro benchmarks to verify the expression compilation performance (previous micro benchmarks primarily focus on execution performance instead of compilation performance).

all micro benchmarks

image

  • The first 6 micro benchmarks are about compilation performance (5 of them are newly added, and one is renamed)
  • This PR is not expected to change execution performance of generated code, so except the first 6, the remaining benchmarks are almost not changed.

The first 6 benchmarks

image

The first 6 benchmarks (log scale)

image

The detailed benchmark stats

before optimization

2024-02-11T15:09:54+08:00
Running release/gandiva-micro-benchmarks
Run on (10 X 24.1211 MHz CPU s)
CPU Caches:
L1 Data 64 KiB
L1 Instruction 128 KiB
L2 Unified 4096 KiB (x10)
Load Average: 2.59, 5.07, 4.75
/Users/ss/dev/projects/opensource/arrow/cpp/src/gandiva/cache.cc:50: Creating gandiva cache with capacity of 500
/Users/ss/dev/projects/opensource/arrow/cpp/src/gandiva/engine.cc:276: Detected CPU Name : apple-m1
/Users/ss/dev/projects/opensource/arrow/cpp/src/gandiva/engine.cc:277: Detected CPU Features: []
--------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------------------------------------
TimedTestExprCompilationNoCache 14760 us 14759 us 39
TimedTestExprCompilationWithCache 227 us 226 us 3094
TimedTestNonBitcodeExprCompilationNoCache 13051 us 13047 us 46
TimedTestNonBitcodeExprCompilationWithCache 238 us 238 us 2916
TimedTestLiteralExprCompilationNoCache 227 us 227 us 2990
TimedTestLiteralExprCompilationWithCache 230 us 230 us 3034
TimedTestAdd3 1134 us 1128 us 635
TimedTestBigNested 7856 us 7854 us 88
TimedTestExtractYear 7183 us 7173 us 98
TimedTestFilterAdd2 2828 us 2828 us 249
TimedTestFilterLike 12836 us 12833 us 55
TimedTestCastFloatFromString 14497 us 14495 us 48
TimedTestCastIntFromString 14271 us 14271 us 49
TimedTestAllocs 34164 us 34164 us 21
TimedTestOutputStringAllocs 51252 us 51230 us 14
TimedTestMultiOr 9022 us 9022 us 78
DecimalAdd2Fast 2054 us 2048 us 348
DecimalAdd2LeadingZeroes 5060 us 5059 us 138
DecimalAdd2LeadingZeroesWithDiv 23955 us 23948 us 29
DecimalAdd2Large 118613 us 118586 us 6
DecimalAdd3Fast 2340 us 2332 us 304
DecimalAdd3LeadingZeroes 8752 us 8751 us 79
DecimalAdd3LeadingZeroesWithDiv 60829 us 60811 us 11
DecimalAdd3Large 241113 us 241100 us 3

after optimization

2024-02-11T15:11:43+08:00
Running release/gandiva-micro-benchmarks
Run on (10 X 24.1228 MHz CPU s)
CPU Caches:
L1 Data 64 KiB
L1 Instruction 128 KiB
L2 Unified 4096 KiB (x10)
Load Average: 2.83, 4.38, 4.51
/Users/ss/dev/projects/opensource/arrow/cpp/src/gandiva/cache.cc:50: Creating gandiva cache with capacity of 500
/Users/ss/dev/projects/opensource/arrow/cpp/src/gandiva/engine.cc:273: Detected CPU Name : apple-m1
/Users/ss/dev/projects/opensource/arrow/cpp/src/gandiva/engine.cc:274: Detected CPU Features: []
--------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------------------------------------
TimedTestExprCompilationNoCache 14382 us 14380 us 39
TimedTestExprCompilationWithCache 82.4 us 82.4 us 8394
TimedTestNonBitcodeExprCompilationNoCache 1255 us 1255 us 499
TimedTestNonBitcodeExprCompilationWithCache 90.6 us 90.6 us 7689
TimedTestLiteralExprCompilationNoCache 82.1 us 82.1 us 8528
TimedTestLiteralExprCompilationWithCache 85.6 us 85.6 us 8167
TimedTestAdd3 1140 us 1133 us 599
TimedTestBigNested 7818 us 7817 us 89
TimedTestExtractYear 7187 us 7184 us 98
TimedTestFilterAdd2 2809 us 2809 us 249
TimedTestFilterLike 13097 us 13093 us 54
TimedTestCastFloatFromString 14168 us 14168 us 49
TimedTestCastIntFromString 14164 us 14159 us 49
TimedTestAllocs 33802 us 33802 us 21
TimedTestOutputStringAllocs 50598 us 50592 us 13
TimedTestMultiOr 11379 us 11378 us 63
TimedTestInExpr 2509 us 2509 us 273
DecimalAdd2Fast 2029 us 2029 us 340
DecimalAdd2LeadingZeroes 5153 us 5151 us 135
DecimalAdd2LeadingZeroesWithDiv 24197 us 24164 us 29
DecimalAdd2Large 118994 us 118917 us 6
DecimalAdd3Fast 2281 us 2280 us 295
DecimalAdd3LeadingZeroes 8937 us 8935 us 78
DecimalAdd3LeadingZeroesWithDiv 60969 us 60966 us 11
DecimalAdd3Large 241916 us 241723 us 3

Conclusion

  • The TimedTestExprCompilationNoCache is slightly faster (around 2% faster) because the compilation is faster but the execution time still dominates this benchmark. It is faster because less C stub functions are defined in the LLVM module
  • The TimedTestExprCompilationWithCache, TimedTestNonBitcodeExprCompilationWithCache and TimedTestLiteralExprCompilationWithCache is faster primarily because we avoid loading the IR and C functions if cache is hit. They are around 2.5x faster.
  • The TimedTestNonBitcodeExprCompilationNoCache and TimedTestLiteralExprCompilationNoCache are 10x and 2.5x faster. For use cases where only C functions are used, such as random(), the compilation should be much faster since LLVM bitcode is not needed to be loaded and linked any more.
  • The generated code's execution performance is expected to remain the same

@niyue
niyueforce-pushed the feature/gdv-engine-perf branch from 6c63093 to bfd3f7dCompareFebruary 12, 2024 13:37
@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting committer review Awaiting committer review labels Feb 13, 2024
@niyue
niyueforce-pushed the feature/gdv-engine-perf branch from bfd3f7d to 200d85cCompareFebruary 14, 2024 02:22
@github-actionsgithub-actionsBot removed the awaiting changes Awaiting changes label Feb 14, 2024
@github-actionsgithub-actionsBot added the awaiting change review Awaiting change review label Feb 14, 2024
…ine can be moved into TMOwningSimplerCompiler, and this avoids the crash during optimization (TargetIRAnalysis for some reason requires its TargetMachine to be alive).
@niyue
niyueforce-pushed the feature/gdv-engine-perf branch from 200d85c to c04f407CompareFebruary 14, 2024 02:29
@github-actions

Copy link
Copy Markdown

Thank you for your contribution. Unfortunately, this pull request has been marked as stale because it has had no activity in the past 365 days. Please remove the stale label or comment below, or this PR will be closed in 14 days. Feel free to re-open this if it has been closed in error. If you do not have repository permissions to reopen the PR, please tag a maintainer.

@github-actionsgithub-actionsBot added the Status: stale-warning Issues and PRs flagged as stale which are due to be closed if no indication otherwise label Nov 18, 2025
@dmitry-chirkov-dremio

Copy link
Copy Markdown
Contributor

@niyue is there still appetite for merging this change?

@niyue

Copy link
Copy Markdown
ContributorAuthor

@dmitry-chirkov-dremio I don’t recall why this PR didn’t move forward at the time, but I can try to pick it up again over the next couple of months and see if I can get it working.

llvm::orc::JITTargetMachineBuilder jtmb(
(llvm::Triple(llvm::sys::getDefaultTargetTriple())));
static auto default_target_triple = llvm::sys::getDefaultTargetTriple();
llvm::orc::JITTargetMachineBuilder jtmb((llvm::Triple(default_target_triple)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this helper whitelist is still incomplete. After this change, a C helper only gets registered if it is either explicitly present in used_functions_ or hardcoded in internal_functions_, but some precompiled IR entry points still call helper stubs transitively.

Two examples I found:

  • castVARCHAR_decimal128_int64 in precompiled/decimal_wrapper.cc calls gdv_fn_dec_to_string
  • castTIMESTAMP_utf8 in precompiled/time.cc calls gdv_fn_time_with_zone

Those helpers are not added to internal_functions_, and they are not top-level expression functions that ExprDecomposer would ever record in used_functions_. That means this optimization can skip registering symbols that the linked IR still needs at runtime.

Can we either keep registering all IR-only helper stubs, or teach this tracking to include transitive C helpers used from precompiled IR?

arrow::MemoryPool* pool_;
};

TEST_F(TestProjector, TestCastDecimalToString) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test seems a bit too weak for the regression it is trying to protect. It only checks that Projector::Make(...) succeeds and that the result was not built from cache, but it never evaluates the expression.

Given the risk here is missing helper registration for decimal-to-string code paths, I’d feel better with an actual Evaluate(...) assertion that exercises the generated code. The timezone-aware castTIMESTAMP_utf8 path may also need similar coverage for the same reason.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Gandiva] Constructing LLVM module with only necessary functions for better performance

3 participants

@niyue@dmitry-chirkov-dremio@kou