[PATCH 13/20] alpha: copy four quads a trip below the ev6 unrolled loop

Matt Turner mattst88@gmail.com
Wed Aug 12 01:19:50 GMT 2026


Copies shorter than 128 bytes never reach the unrolled loop, and the
remainder of the ones that do is up to 63 bytes; both were handled a
quadword at a time by a loop spending eight issue slots per eight bytes.
That is the wrong shape for the work.  Short copies miss nothing, replay
nothing and retire better than two instructions a cycle, so unlike the long
copies this file spends most of its comments on they are issue-bound, and
what they want is fewer instructions per byte.

Take four quads a trip while at least 32 bytes remain, then fall into the
existing one-quad loop.  Measured on an EV68CB, hot, cycles per call:

  size    before   after
  112       64.7    36.2   1.79x
  120       65.9    37.7   1.75x
  127       69.4    42.2   1.64x
  160       80.0    52.8   1.52x
  96        40.3    31.7   1.27x
  64        32.2    27.1   1.19x
  128       52.8    47.8   1.11x

The loop body sits out of line, past every return path, and that placement
is not incidental.  With it inline just above $move_a_quad, copies of 1KB
and 2KB lost about 5% -- 228 cycles becoming 237 at 1KB -- even though a
copy that size never executes a single one of these instructions.  Growing
the straight-line distance between the unrolled loop and the tail is enough
to do that on its own.  Out of line, 1KB gains 4% and 2KB is level, and the
only price is a few percent of the gain between 32 and 96 bytes.

Copies below 32 bytes pay one cycle for the test that skips the new loop --
about 5% at 8 to 24 bytes, where the call is only 17 to 22 cycles to begin
with.  The instruction count at the head is unchanged, since the test takes
the place of two nops; the cost is the branch itself.  Note that GCC expands
small constant-size copies inline, so the lengths that lose here are the
ones least likely to reach this code at all.

Sizes from 256 to 512 still measure 2-3% down for the same layout reason,
which no placement tried made go away entirely.

Verified on an EV68CB against the C memcpy and memmove for every length
from 0 to 600 at 72 destination alignments, and for memmove across source-
destination gaps from 8 bytes to 1KB, with 512-byte guard regions checked
for stray writes.
---
 sysdeps/alpha/alphaev6/memcpy.S  | 45 ++++++++++++++++++++++++++++++--
 sysdeps/alpha/alphaev6/memmove.S | 45 ++++++++++++++++++++++++++++++--
 2 files changed, 86 insertions(+), 4 deletions(-)

diff --git ./sysdeps/alpha/alphaev6/memcpy.S ./sysdeps/alpha/alphaev6/memcpy.S
index ee251e8b8f..d86c00b9be 100644
--- ./sysdeps/alpha/alphaev6/memcpy.S
+++ ./sysdeps/alpha/alphaev6/memcpy.S
@@ -253,8 +253,8 @@ $no_unroll:
 	.align 4
 	subq	$18, 8, $18		# E : At least a quad left?
 	blt	$18, $less_than_8	# U : Nope
-	nop				# E :
-	nop				# E :
+	subq	$18, 24, $1		# E : four whole quads left?
+	bge	$1, $move_four_quads	# U : yes -- out of line, below
 
 $move_a_quad:
 	ldq	$1, 0($17)		# L : fetch 8
@@ -402,6 +402,47 @@ $nomoredata:
 	nop				# E :
 	nop				# E :
 
+
+	/*
+	 * Four quads a trip, for the 32..127 byte copies that never reach the
+	 * unrolled loop and for the remainder of the ones that do.  Short
+	 * copies are issue-bound -- they miss nothing and replay nothing, and
+	 * retire better than two instructions a cycle -- so what they want is
+	 * simply fewer instructions per byte than the one-quad loop above.
+	 *
+	 * This lives out of line, past every return path, on purpose.  Placed
+	 * inline just above $move_a_quad it costs long copies about 5% at 1KB
+	 * and 2KB, even though a copy that size never executes a single one of
+	 * these instructions, purely by pushing the unrolled loop and the tail
+	 * further apart.  Moving it down here recovers that and gives up only
+	 * a few percent of the gain between 32 and 96 bytes.
+	 *
+	 * $18 counts bytes remaining less eight, so a full trip needs $18 >= 24.
+	 * Falling out can leave $18 negative and $move_a_quad is a do-while, so
+	 * the count is rechecked before going back to it.
+	 */
+	.align 4
+$move_four_quads:
+	ldq	$1, 0($17)		# L : 32 bytes
+	ldq	$2, 8($17)		# L :
+	ldq	$3, 16($17)		# L :
+	ldq	$5, 24($17)		# L :
+
+	stq	$1, 0($16)		# L :
+	stq	$2, 8($16)		# L :
+	stq	$3, 16($16)		# L :
+	stq	$5, 24($16)		# L :
+
+	addq	$17, 32, $17		# E : src += 32
+	addq	$16, 32, $16		# E : dest += 32
+	subq	$18, 32, $18		# E : count -= 32
+	nop				# E :
+
+	subq	$18, 24, $1		# E : room for another full trip?
+	bge	$1, $move_four_quads	# U :
+	bge	$18, $move_a_quad	# U : a whole quad still left
+	br	$31, $less_than_8	# U : only trailing bytes
+
 END(memcpy)
 #ifndef USE_AS_MEMPCPY
 libc_hidden_builtin_def (memcpy)
diff --git ./sysdeps/alpha/alphaev6/memmove.S ./sysdeps/alpha/alphaev6/memmove.S
index d587a90dd9..a69a0588a2 100644
--- ./sysdeps/alpha/alphaev6/memmove.S
+++ ./sysdeps/alpha/alphaev6/memmove.S
@@ -237,8 +237,8 @@ $no_unroll:
 	.align 4
 	subq	$18, 8, $18		# E : At least a quad left?
 	blt	$18, $less_than_8	# U : Nope
-	nop				# E :
-	nop				# E :
+	subq	$18, 24, $1		# E : four whole quads left?
+	bge	$1, $move_four_quads	# U : yes -- out of line, below
 
 $move_a_quad:
 	ldq	$1, 0($17)		# L : fetch 8
@@ -412,5 +412,46 @@ $back_b:
 	bne	$18, $back_b		# U :
 	ret	$31, ($26), 1		# L0 :
 
+
+	/*
+	 * Four quads a trip, for the 32..127 byte copies that never reach the
+	 * unrolled loop and for the remainder of the ones that do.  Short
+	 * copies are issue-bound -- they miss nothing and replay nothing, and
+	 * retire better than two instructions a cycle -- so what they want is
+	 * simply fewer instructions per byte than the one-quad loop above.
+	 *
+	 * This lives out of line, past every return path, on purpose.  Placed
+	 * inline just above $move_a_quad it costs long copies about 5% at 1KB
+	 * and 2KB, even though a copy that size never executes a single one of
+	 * these instructions, purely by pushing the unrolled loop and the tail
+	 * further apart.  Moving it down here recovers that and gives up only
+	 * a few percent of the gain between 32 and 96 bytes.
+	 *
+	 * $18 counts bytes remaining less eight, so a full trip needs $18 >= 24.
+	 * Falling out can leave $18 negative and $move_a_quad is a do-while, so
+	 * the count is rechecked before going back to it.
+	 */
+	.align 4
+$move_four_quads:
+	ldq	$1, 0($17)		# L : 32 bytes
+	ldq	$2, 8($17)		# L :
+	ldq	$3, 16($17)		# L :
+	ldq	$5, 24($17)		# L :
+
+	stq	$1, 0($16)		# L :
+	stq	$2, 8($16)		# L :
+	stq	$3, 16($16)		# L :
+	stq	$5, 24($16)		# L :
+
+	addq	$17, 32, $17		# E : src += 32
+	addq	$16, 32, $16		# E : dest += 32
+	subq	$18, 32, $18		# E : count -= 32
+	nop				# E :
+
+	subq	$18, 24, $1		# E : room for another full trip?
+	bge	$1, $move_four_quads	# U :
+	bge	$18, $move_a_quad	# U : a whole quad still left
+	br	$31, $less_than_8	# U : only trailing bytes
+
 END(memmove)
 libc_hidden_builtin_def (memmove)
-- 
2.54.0



More information about the Libc-alpha mailing list