Files
Groene AIandClaude Opus 5.5 340383405a Stabilize the nested-array queries of array_first_last
Query #7 of tests/performance/array_first_last.xml (arrayLast over
Array(Array(UInt64))) takes a different time in each server process of
the same build. In 14 days of Performance Comparison runs on amd it moved
by more than 15% between the two servers in 50 of 633 runs, 36 of them
master against master, while #6 (arrayFirst over the same column) moved
in 0 of 641 and the tuple queries #4/#5 in 2 and 3; arm is quiet. Per
run, #6 and #7 move in opposite directions (r = -0.53 over 524 amd runs;
in 33 of the 37 runs where #7 moved by more than 15%, #6 moved the other
way), while #4/#5 move together (r = +0.42).

Each nested_values row holds 32 inner arrays of 2 UInt64, so rows are 256
bytes apart in the inner offsets and 512 bytes apart in the data. #6 and
#7 read one inner array per row, so for the whole query every row is read
at the same position modulo 256/512 bytes, i.e. at the same few offsets
inside every memory page. Which offsets those are depends on where the
column is allocated: jemalloc is built with cache_oblivious, which puts
every large allocation at a random multiple of 64 bytes inside its first
page, and the table is filled once per server. So each server reads the
column at its own fixed page offsets.

Fill nested_values with 31 inner arrays per row. Rows are then 248/496
bytes apart and the read position walks through every offset of the page
within one query, so the timing no longer depends on the allocation. The
query texts and the per-row work (one copy of a 2-element array per row,
250000 rows) are unchanged.

Local check with the official master build 26.10.1.906 on a Xeon 6975P-C:
16 separately filled copies of the table per arm in one server, measured
round-robin for 20 rounds. This machine does not show the spread with
either layout (#7 across copies: 14.14-14.68 ms with 32 elements and
random allocation offsets, 13.93-14.27 ms page-aligned with
MALLOC_CONF=cache_oblivious:false, 13.95-14.32 ms with 31 elements), so
it cannot confirm the mechanism and the change rests on the CI data. The
level of #7 stays (31 vs 32 elements: 0.98x); #7/#6 goes from 1.11 to
1.03.

Related: #120510

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:11:53 +00:00

79 lines
3.0 KiB
XML

<test>
<!--
Populate the arrays before the timed queries. Constructing temporary arrays with arrayResize inside
the query would add allocation and copy work that is unrelated to arrayFirst/arrayLast.
-->
<settings>
<max_threads>1</max_threads>
</settings>
<create_query>
CREATE TABLE array_first_last_32
(
x Array(UInt64)
)
ENGINE = Memory
</create_query>
<create_query>
CREATE TABLE array_first_last_128
(
x Array(UInt64)
)
ENGINE = Memory
</create_query>
<create_query>
CREATE TABLE array_first_last_complex
(
tuple_values Array(Tuple(UInt64, UInt64)),
nested_values Array(Array(UInt64))
)
ENGINE = Memory
</create_query>
<fill_query>
INSERT INTO array_first_last_32
SELECT arrayResize([toUInt64(number)], 32)
FROM numbers(2000000)
</fill_query>
<fill_query>
INSERT INTO array_first_last_128
SELECT arrayResize([toUInt64(number)], 128)
FROM numbers(500000)
</fill_query>
<!--
nested_values rows have 31 elements: with a power-of-two row size every row is read at the same few offsets
within a memory page, and which offsets depends on where the column is allocated in each server.
-->
<fill_query>
INSERT INTO array_first_last_complex
SELECT
arrayMap(x -&gt; (x, x), range(32)),
arrayMap(x -&gt; [x, x], range(31))
FROM numbers(250000)
</fill_query>
<!-- Constant predicate path. -->
<query>SELECT arrayFirst(x -&gt; 1, x) FROM array_first_last_32 FORMAT Null</query>
<query>SELECT arrayLast(x -&gt; 1, x) FROM array_first_last_32 FORMAT Null</query>
<query>SELECT arrayFirst(x -&gt; 1, x) FROM array_first_last_128 FORMAT Null</query>
<query>SELECT arrayLast(x -&gt; 1, x) FROM array_first_last_128 FORMAT Null</query>
<query>SELECT arrayFirst(x -&gt; 1, tuple_values) FROM array_first_last_complex FORMAT Null</query>
<query>SELECT arrayLast(x -&gt; 1, tuple_values) FROM array_first_last_complex FORMAT Null</query>
<query>SELECT arrayFirst(x -&gt; 1, nested_values) FROM array_first_last_complex FORMAT Null</query>
<query>SELECT arrayLast(x -&gt; 1, nested_values) FROM array_first_last_complex FORMAT Null</query>
<!-- Regular filter path: each function finds its match, if any, at the first element it checks. -->
<query>SELECT arrayFirst(x -&gt; x &gt; 3, x) FROM array_first_last_32 FORMAT Null</query>
<query>SELECT arrayLast(x -&gt; x = 0, x) FROM array_first_last_32 FORMAT Null</query>
<query>SELECT arrayFirst(x -&gt; x &gt; 3, x) FROM array_first_last_128 FORMAT Null</query>
<query>SELECT arrayLast(x -&gt; x = 0, x) FROM array_first_last_128 FORMAT Null</query>
<drop_query>DROP TABLE IF EXISTS array_first_last_32</drop_query>
<drop_query>DROP TABLE IF EXISTS array_first_last_128</drop_query>
<drop_query>DROP TABLE IF EXISTS array_first_last_complex</drop_query>
</test>