Skip to content

Missed optimization: fold strlen over selected constant strings feeding inlined std::string copy #4

Description

@ParkHanbum

Summary

LLVM currently leaves a runtime strlen(selected_ptr) call when
selected_ptr is selected from constant string literals and the selected
pointer plus the length are consumed together by inlined std::string
construction/copy logic.

The missed opportunity is to derive the matching selected length from the same
control/data flow:

selected_ptr = phi/select over constant string leaves
len = strlen(selected_ptr)

can become:

selected_ptr = same selected pointer
selected_len = equivalent phi/select/table over first-NUL lengths

The selected pointer identity must be preserved; only the runtime length
computation is replaced.

Reduced Reproducer

Input:

target triple = "arm64-apple-darwin24.3.0"

@s0 = private unnamed_addr constant [3 x i8] c"ok\00", align 1
@s1 = private unnamed_addr constant [8 x i8] c"missing\00", align 1
@s2 = private unnamed_addr constant [10 x i8] c"cancelled\00", align 1
@s3 = private unnamed_addr constant [11 x i8] c"timed out!\00", align 1
@s4 = private unnamed_addr constant [14 x i8] c"unavailable!!\00", align 1
@s5 = private unnamed_addr constant [20 x i8] c"already initialized\00", align 1
@s6 = private unnamed_addr constant [24 x i8] c"permission denied here!\00", align 1
@s7 = private unnamed_addr constant [26 x i8] c"resource exhausted state!\00", align 1

declare i64 @strlen(ptr)
declare void @llvm.memcpy.p0.p0.i64(ptr nocapture writeonly, ptr nocapture readonly, i64, i1 immarg)

define i64 @select_len_baseline(i32 %x, ptr nocapture writeonly %dst) noinline {
entry:
  %idx = and i32 %x, 7
  switch i32 %idx, label %case0 [
    i32 1, label %case1
    i32 2, label %case2
    i32 3, label %case3
    i32 4, label %case4
    i32 5, label %case5
    i32 6, label %case6
    i32 7, label %case7
  ]

case0:
  br label %join
case1:
  br label %join
case2:
  br label %join
case3:
  br label %join
case4:
  br label %join
case5:
  br label %join
case6:
  br label %join
case7:
  br label %join

join:
  %selected = phi ptr [ @s0, %case0 ], [ @s1, %case1 ], [ @s2, %case2 ], [ @s3, %case3 ], [ @s4, %case4 ], [ @s5, %case5 ], [ @s6, %case6 ], [ @s7, %case7 ]
  %len = call i64 @strlen(ptr nonnull dereferenceable(1) %selected)
  %is_big = icmp ugt i64 %len, 15
  br i1 %is_big, label %big, label %small

big:
  call void @llvm.memcpy.p0.p0.i64(ptr align 1 %dst, ptr nonnull align 1 %selected, i64 %len, i1 false)
  br label %done

small:
  switch i64 %len, label %small_copy [
    i64 0, label %done
    i64 1, label %one
  ]

one:
  %c = load i8, ptr %selected, align 1
  store i8 %c, ptr %dst, align 1
  br label %done

small_copy:
  call void @llvm.memcpy.p0.p0.i64(ptr align 1 %dst, ptr nonnull align 1 %selected, i64 %len, i1 false)
  br label %done

done:
  %term = getelementptr i8, ptr %dst, i64 %len
  store i8 0, ptr %term, align 1
  ret i64 %len
}

Run:

opt -S '-passes=default<O2>' repro.ll -o -

Actual result still contains a runtime strlen:

%switch.load = load ptr, ptr %switch.gep, align 8
%len = tail call i64 @strlen(ptr noundef nonnull dereferenceable(1) %switch.load)
...
tail call void @llvm.memcpy.p0.p0.i64(ptr align 1 %dst, ptr nonnull align 1 %switch.load, i64 %len, i1 false)

Expected Result

LLVM should be able to derive an equivalent selected length for eligible
constant-string domains:

selected_len = select/phi/table over [2, 7, 9, 10, 13, 19, 23, 25]

and use that value in the copy length path instead of calling
strlen(selected_ptr).

A manually transformed version of the reproducer optimizes to a form with no
strlen call:

%switch.load = load ptr, ptr %switch.gep, align 8
%switch.load3 = load i64, ptr %switch.gep2, align 8
tail call void @llvm.memcpy.p0.p0.i64(ptr ... %dst, ptr ... %switch.load, i64 %switch.load3, i1 false)
%term = getelementptr i8, ptr %dst, i64 %switch.load3
ret i64 %switch.load3

Real-World Evidence

A focused scan over optimized LLVM IR benchmark inputs found this corrected
target shape in:

  • 80 hits
  • 54 files
  • 14 projects

Domain breakdown:

  • phi_select: 44
  • phi: 19
  • nested_select: 17

Seven representative hits were replayed with current default<O2>. All seven
still preserved strlen(selected_ptr) and were reviewed as valid optimization
pattern candidates.

Reviewed examples include:

Source Domain Lengths
bench/arrow/optimized/status.ll:742 phi [2, 7, 9, 10, 11, 13, 14, 19, 23, 25]
bench/llvm/optimized/AttributorAttributes.ll:135779 phi_select [10, 12]
bench/llvm/optimized/AttributorAttributes.ll:152260 nested_select [12, 18]
bench/flatbuffers/optimized/idl_gen_cpp.ll:38750 phi_select [7, 8, 9]
bench/verilator/optimized/V3AstNodes.ll:165149 nested_select [11, 12, 13, 14]
bench/yaml-cpp/optimized/emitter.ll:3143 phi_select [1, 2, 3, 4, 5]
bench/glslang/optimized/Initialize.ll:45485 phi [3, 5, 9]

The final corrected scan excludes earlier overmatches such as generic Graphviz
agxb buffer append cases. Those also had strlen(selected_ptr) plus copy
uses, but they were not inlined std::string construction/copy cases. The
corrected evidence count is 80, not the intermediate 116-hit or 83-hit scanner
outputs.

Existing Optimization Boundary

LLVM already handles a narrower direct-select shape such as:

strlen(select %cond, @literal_a, @literal_b)

This report is about surviving PHI, PHI/select-chain, and nested-select domains
that feed inlined std::string construction/copy logic and still preserve the
runtime strlen(selected_ptr) after default<O2>.

Legality / Scope

A valid transform should require:

  • all selected leaves are constant strings with known first-NUL lengths;
  • the selected pointer passed to strlen is the same selected pointer used as
    the copy source;
  • PHI incoming edges and select conditions are preserved when constructing the
    selected length;
  • integer width and extension/truncation behavior are correct for the length
    uses;
  • poison/freeze behavior is not worsened;
  • loaded, mutable, unknown, or non-constant pointer domains are rejected;
  • interior pointers are handled only when the offset and first-NUL length are
    proven.

Out of scope:

  • generic dynamic strlen folding;
  • generic strlen plus memcpy buffer append cases;
  • Graphviz agxb buffer append cases;
  • std::__ostream_insert call-pair cases;
  • loaded pointer domains;
  • mutable or otherwise unproven string storage.

Performance Support

I also checked a local POC comparing the current baseline against a manual
selected-length transform. This is supporting evidence only, not a production
benchmark claim.

Native timing smoke on arm64 Darwin, with matching checksums:

baseline: 11.389 ns/iter
manual:    7.574 ns/iter
speedup:   about 1.50x

Static llvm-mca --iterations=100 smoke also favored the manual form on
measured non-wasm targets:

Target Baseline Block RThroughput Manual Block RThroughput
x86-64/skylake 7.3 5.0
AArch64/neoverse-n1 13.0 8.7
RISC-V/sifive-u74 19.0 13.5

llvm-mca models calls approximately, so the key signal is directional: the
manual selected-length form removes the strlen call and reduces modeled
instruction/uop/throughput pressure.

Appendix : all pattern found

Details
# Project File Line
1 arrow bench/arrow/optimized/status.ll 742
2 cmake bench/cmake/optimized/cmCommonTargetGenerator.ll 644
3 cmake bench/cmake/optimized/cmCommonTargetGenerator.ll 995
4 flatbuffers bench/flatbuffers/optimized/bfbs_gen_lua.ll 28215
5 flatbuffers bench/flatbuffers/optimized/bfbs_gen_nim.ll 23478
6 flatbuffers bench/flatbuffers/optimized/idl_gen_cpp.ll 38750
7 flatbuffers bench/flatbuffers/optimized/idl_gen_cpp.ll 91517
8 flatbuffers bench/flatbuffers/optimized/idl_gen_cpp.ll 111888
9 flatbuffers bench/flatbuffers/optimized/idl_gen_csharp.ll 34292
10 flatbuffers bench/flatbuffers/optimized/idl_gen_go.ll 59638
11 flatbuffers bench/flatbuffers/optimized/idl_gen_lobster.ll 21855
12 flatbuffers bench/flatbuffers/optimized/idl_gen_python.ll 49874
13 flatbuffers bench/flatbuffers/optimized/idl_gen_rust.ll 12647
14 flatbuffers bench/flatbuffers/optimized/idl_gen_rust.ll 18520
15 flatbuffers bench/flatbuffers/optimized/idl_gen_rust.ll 23213
16 flatbuffers bench/flatbuffers/optimized/idl_gen_swift.ll 8337
17 flatbuffers bench/flatbuffers/optimized/idl_gen_swift.ll 10351
18 flatbuffers bench/flatbuffers/optimized/idl_gen_swift.ll 16535
19 flatbuffers bench/flatbuffers/optimized/idl_gen_swift.ll 39248
20 flatbuffers bench/flatbuffers/optimized/idl_gen_swift.ll 64872
21 folly bench/folly/optimized/json.ll 12624
22 glslang bench/glslang/optimized/Initialize.ll 45485
23 glslang bench/glslang/optimized/ParseContextBase.ll 5216
24 glslang bench/glslang/optimized/ParseHelper.ll 11247
25 glslang bench/glslang/optimized/SymbolTable.ll 5045
26 glslang bench/glslang/optimized/glslang_tab.ll 48320
27 glslang bench/glslang/optimized/hlslParseHelper.ll 10963
28 glslang bench/glslang/optimized/hlslParseables.ll 2592
29 glslang bench/glslang/optimized/hlslParseables.ll 2713
30 glslang bench/glslang/optimized/hlslParseables.ll 2834
31 glslang bench/glslang/optimized/intermOut.ll 8776
32 glslang bench/glslang/optimized/linkValidate.ll 8614
33 gromacs bench/gromacs/optimized/colvardeps.ll 2056
34 gromacs bench/gromacs/optimized/gmx_density.ll 2605
35 gromacs bench/gromacs/optimized/gmx_densmap.ll 2247
36 grpc bench/grpc/optimized/channel_trace.ll 707
37 grpc bench/grpc/optimized/sockaddr_utils.ll 2779
38 llvm bench/llvm/optimized/ArrayBoundCheckerV2.ll 2793
39 llvm bench/llvm/optimized/ArrayBoundCheckerV2.ll 2822
40 llvm bench/llvm/optimized/ArrayBoundCheckerV2.ll 2898
41 llvm bench/llvm/optimized/AttributorAttributes.ll 135779
42 llvm bench/llvm/optimized/AttributorAttributes.ll 152260
43 llvm bench/llvm/optimized/CGObjCMac.ll 42260
44 llvm bench/llvm/optimized/ClangOpenCLBuiltinEmitter.ll 3116
45 llvm bench/llvm/optimized/Darwin.ll 32618
46 llvm bench/llvm/optimized/GCOV.ll 9395
47 llvm bench/llvm/optimized/GlobalISelMatchTable.ll 22361
48 llvm bench/llvm/optimized/GlobalISelMatchTable.ll 27843
49 llvm bench/llvm/optimized/ItaniumMangle.ll 19019
50 llvm bench/llvm/optimized/LoopAccessAnalysis.ll 13732
51 llvm bench/llvm/optimized/NumberObjectConversionChecker.ll 23660
52 llvm bench/llvm/optimized/RegisterInfoEmitter.ll 11684
53 llvm bench/llvm/optimized/SubtargetEmitter.ll 1819
54 llvm bench/llvm/optimized/SveEmitter.ll 13975
55 minetest bench/minetest/optimized/guiFormSpecMenu.ll 28499
56 ocio bench/ocio/optimized/CTFTransform.ll 18833
57 proj bench/proj/optimized/crs.ll 28398
58 proj bench/proj/optimized/datum.ll 5942
59 verilator bench/verilator/optimized/V3AstNodes.ll 165149
60 verilator bench/verilator/optimized/V3AstNodes.ll 165466
61 verilator bench/verilator/optimized/V3AstNodes.ll 181946
62 verilator bench/verilator/optimized/V3AstNodes.ll 184352
63 verilator bench/verilator/optimized/V3AstNodes.ll 188462
64 verilator bench/verilator/optimized/V3AstNodes.ll 204891
65 verilator bench/verilator/optimized/V3EmitCImp.ll 28440
66 verilator bench/verilator/optimized/V3EmitCImp.ll 29867
67 verilator bench/verilator/optimized/V3EmitCImp.ll 83358
68 verilator bench/verilator/optimized/V3EmitCModel.ll 26400
69 verilator bench/verilator/optimized/V3EmitCModel.ll 27827
70 verilator bench/verilator/optimized/V3EmitCModel.ll 73334
71 verilator bench/verilator/optimized/V3EmitCSyms.ll 44352
72 verilator bench/verilator/optimized/V3EmitV.ll 13925
73 verilator bench/verilator/optimized/V3EmitV.ll 14116
74 verilator bench/verilator/optimized/V3EmitV.ll 17748
75 verilator bench/verilator/optimized/V3OrderGraphBuilder.ll 6205
76 verilator bench/verilator/optimized/V3Timing.ll 24588
77 verilator bench/verilator/optimized/V3Tristate.ll 14169
78 yaml-cpp bench/yaml-cpp/optimized/emitter.ll 3143
79 yosys bench/yosys/optimized/extract.ll 7834
80 yosys bench/yosys/optimized/show.ll 3062

need to be organization
bench/openspiel/optimized/bridge.ll:2227
bench/openspiel/optimized/bridge.ll:2447
bench/openspiel/optimized/bridge.ll:4558
bench/assimp/optimized/glTFImporter.ll:77565
bench/assimp/optimized/glTFExporter.ll:52694
bench/grpc/optimized/flow_control.ll:205
bench/grpc/optimized/flow_control.ll:330
bench/grpc/optimized/channel_init.ll:5125
bench/llvm/optimized/LoopVectorize.ll:71106
bench/cmake/optimized/cmGlobalGenerator.ll:38397
bench/cmake/optimized/cmComputeLinkInformation.ll:3879
bench/cmake/optimized/cmStandardLevelResolver.ll:891
bench/cmake/optimized/cmQtAutoGenerator.ll:587
bench/cmake/optimized/cmQtAutoGenerator.ll:1060
bench/cmake/optimized/cmQtAutoGenerator.ll:1286
bench/cmake/optimized/cmQtAutoGenerator.ll:1466
bench/llvm/optimized/VPlan.ll:13376
bench/llvm/optimized/X86AsmParser.ll:8694
bench/llvm/optimized/X86AsmParser.ll:9540
bench/llvm/optimized/X86AsmParser.ll:13393
bench/llvm/optimized/HIPUtility.ll:5294
bench/llvm/optimized/MachineVerifier.ll:28155
bench/llvm/optimized/BreakableToken.ll:716
bench/llvm/optimized/SemaCodeComplete.ll:21173
bench/llvm/optimized/CommonArgs.ll:31850
bench/llvm/optimized/MachineFunctionPass.ll:810
bench/hermes/optimized/JSRegExp.ll:796
bench/hermes/optimized/FileCheck.ll:3179

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions