Skip to content

cmd/compile: rewrite []byte(string([]byte)) to makeslicecopy on riscv64 - #165

Open
ctk-1998 wants to merge 1 commit into
go1.26.5-zte-devfrom
bytesstringbytesopt
Open

ctk-1998 wants to merge 1 commit into
go1.26.5-zte-devfrom
bytesstringbytesopt

Conversation

@ctk-1998

Copy link
Copy Markdown
Collaborator

Related Issue(s) & Descriptions

Original pr: https://atomgit.com/openeuler/golang/pull/77, authored by rfwang07.

Port the ARM64-only optimization that folds []byte(string([]byte)) into a single runtime.makeslicecopy call to riscv64.

The pattern normally lowers into two runtime calls, each allocating and copying the data:

runtime.slicebytetostring(buf, ptr, len)  // alloc + copy #1
runtime.stringtoslicebyte(buf, sptr, sl)  // alloc + copy #2

The new pass runs after lowering and replaces the pair with a single runtime.makeslicecopy call, halving both heap allocations and copies:

runtime.makeslicecopy(&type:uint8, len, len, ptr)

The rewrite is gated behind -bytesstringbytesopt (off by default) and only fires when the conversion escapes to the heap (tmpBuf == nil), so small conversions that use a stack temporary are left unchanged.

The port maps the ARM64-specific SSA ops to their riscv64 equivalents (OpARM64CALLstatic -> OpRISCV64CALLstatic, OpARM64MOVDconst -> OpRISCV64MOVDconst, OpARM64MOVDaddr -> OpRISCV64MOVaddr) and adapts the codegen assertion to the riscv64 JAL call mnemonic.

Tests

Benchmarks on sg2044:

goos: linux
goarch: riscv64
pkg: cmd/compile/internal/ssa
                                       │ 0821_sg_old.txt │          0821_sg_new.txt           │
                                       │     sec/op      │   sec/op     vs base               │
BytesStringBytesSubslice                    15.887µ ± 4%   7.976µ ± 2%  -49.80% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=8            96.05n ± 0%   80.04n ± 1%  -16.68% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=32           157.7n ± 1%   183.8n ± 2%  +16.51% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=128          849.4n ± 2%   428.9n ± 2%  -49.51% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=512          2.562µ ± 2%   1.288µ ± 4%  -49.74% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=2048         9.070µ ± 3%   4.669µ ± 2%  -48.53% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=8192         36.51µ ± 3%   18.65µ ± 3%  -48.91% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=32768       129.98µ ± 5%   64.71µ ± 9%  -50.22% (p=0.002 n=6)
BytesStringBytesSubsliceSizes/n=131072       474.7µ ± 7%   266.3µ ± 7%  -43.90% (p=0.002 n=6)
geomean                                      6.040µ        3.583µ       -40.69%

Why does the benchmark regress at n=32?

Measurements confirm that allocs/op drops from 2 to 1 and B/op is roughly halved — the optimization genuinely saves work (one allocation and one copy); it simply doesn't translate into faster wall-clock time. The likely cause is a code-layout effect: enabling the optimization changes the machine code size of the hot function (or hot loop), so the critical call/branch instructions in the loop land on different 64-byte cache line boundaries, altering CPU front-end instruction fetch efficiency.

The absolute time at n=32 is only 158 ns. At that scale, the "one mallocgc + one memmove" saved by the optimization amounts to only a few tens of nanoseconds, while the alignment/fetch effect caused by the change in code size is also on the order of tens of nanoseconds. The two are comparable, so the net direction is essentially random: n=8 comes out slightly faster (−16.68%) while n=32 comes out slightly slower (+16.51%). From n≥128 onward, allocation and copying dominate the runtime, drowning out the layout noise, and the optimization consistently shows ~−49%.

Checklist

  • Tests were added or are not required
  • Documentation was added or is not required

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant