Skip to content

Core, Spark: Add JMH benchmarks for Variants - #15629

Open
steveloughran wants to merge 7 commits into
apache:mainfrom
steveloughran:pr/benchmark-variant
Open

Core, Spark: Add JMH benchmarks for Variants#15629
steveloughran wants to merge 7 commits into
apache:mainfrom
steveloughran:pr/benchmark-variant

Conversation

@steveloughran

@steveloughransteveloughran commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Fixes#15628

core:VariantSerializationBenchmark

Separate benchmarks for

  • serializing a prebuilt object
  • deserializing

Variables are:

  • depth: [shallow, nested, deep-nested]
  • percentage of fields shed [0, 33, 67, 100]

spark-4.1:IcebergSourceVariantReadBenchmark

Generate Avro, unshedded Parquet and shedded Parquet tables with the same variant data and then compare performance for basic filter and project operations against the normal columns and the variant fields.

Key findings:

  • although it has the smallest file size, parquet files with shredded variants have significantly worse performance when working with the variant structs than unshredded.
  • Avro is best for the variant data, though all operations will have to read the entire file, operations on other columns are (as expected) slower.
  • Filtering is the operation which is slow. Projecting on an variant column, shredded or unshredded, is as fast as projecting normal parquet column.

I'm not reaching any conclusion why this is the case. I am looking at improving the performance of reconstructing string fields in parquet-java as those benchmark show needless byte-string-byte conversion. For the iceberg benchmark and layers below, I think knowing where issues like is enough of a change.

Writeup

See https://steveloughran.github.io/benchmarking-variants/ for the writeup and the interactive benchmark results of Iceberg and Parquet benchmarks.

@steveloughransteveloughran changed the title Add JMH benchmarks for VariantsCore: Add JMH benchmarks for VariantsMar 13, 2026
@steveloughran
steveloughran marked this pull request as draft March 16, 2026 16:52
@steveloughransteveloughran changed the title Core: Add JMH benchmarks for VariantsCore, Spark: Add JMH benchmarks for VariantsMar 24, 2026
@steveloughran

steveloughran commented Apr 1, 2026

Copy link
Copy Markdown
ContributorAuthor

@rashworld-max still a WiP I'm afraid. Need to know I'm measuring the right thing. Also I can't tell from your profile whether or not you are a human.


class ParquetVariantUtil {
@VisibleForTesting
public final class ParquetVariantUtil {

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.

Is it possible to relocate the tests rather than expose this? We can do this, but generally prefer not to if we can avoid it.

@steveloughransteveloughranApr 12, 2026

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.

happy to do that...I have done it in the parquet PR

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ho

@steveloughran
steveloughran marked this pull request as ready for review April 14, 2026 20:03
@steveloughran

Copy link
Copy Markdown
ContributorAuthor

I think this is ready for review. I've got the initial results and it's good for PRs like #3477 to be able to before/after benchmarks.

More stuff can go in later; I've outlined them in my report. Equality deletes would be a fun one

*/
private long materializeNonEmpty(String operation, Dataset<?> ds) {
LOG.info("{} table={}", operation, tableType);
final long count = ds.count();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Spark doesn't need to evaluate projection to count records.

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.

I needed something to do the entire compute and count() worked. Otherwise it's evaluate every row and feed to a black hole. What do you prefer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Spark can count records without evaluating projection so it's not really testing the projection here.

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.

it seemed to work, but I will get and discard each record instead

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rashworld-max

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.

@manuzhang fyi, now using the same sequence as the IcebergSourceBenchmark superclass, with the retention of the count for use in assertions

 final long count = ds.queryExecution().toRdd().toJavaRDD().count();
blackhole.consume(count);

"variant_get(nested, '$.varcategory', 'int')";

/** Get the ID field from inside the variant: {@value}. */
private static final String VARIANT_GET_NESTED_ID = "variant_get(nested, '$.varid', 'int')";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this be int64 as in the comments above?

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.

will review

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.

moved to bigint. Not doing enough with that type to measure any difference between that and int processing in the workflow.

@steveloughran
steveloughran marked this pull request as draft May 8, 2026 17:02
@steveloughran

Copy link
Copy Markdown
ContributorAuthor

switching to draft again as I'm reworking the benchmark to show variant rowgroup filtering of shredded variants works (#15510), with changes including

  • move to single thread spark worker (less variance in results)
  • going to a single large file with multiple rowgroups
  • exploring the difference between variant_get(...) is 5 and variant_get(...) in (5) to see if spark is treating them differently.

Needs an extra PR in iceberg from qlong and a snaspshot of spark 4.1 with his changes for spark to pass variant_get down

So, surprisingly complex. If I can show the chain works then it's time to start with the feature merges in spark and then here.

This branch will merge without direct dependency, it's just a key goal of the spark benchmark is "show pushdown working". It's not ready to merge unless it can do that

Fixesapache#15628
Core: benchmark of variant creation and ser/deser costs.
Separate benchmarks for
* building
* serializing a prebuilt object
* deserializing
Variables are:
- fields: [1000, 10000]
- depth: [shallow, nested]
- percentage of fields shed [0, 33, 67, 100]
Note: the current benchmark does NOT for the JVM, as it allows for fast iterative development.
A final merge should switch to fork(1).
Spark:
Full test of predicate pushdown of variants
- avro
- parquet unshredded
- parquet shredded
For this to return useful numbers, requires PRs for
- Passing down variant_get between spark and iceberg
- ParquetRowGroupFilter to filter on shredded variants.
contains
Add some more benchmarks
Change-Id: I4231280f08cf63db5960ecb79301ae9458b35272
* file skipping can be observed as out of range equals/element tests are skipped completely
* varcat filters still very slow
Changes
- blackhole consumption of rows
- wiring up to ParquetMetricsRowGroupFilter.resetShreddedMetricsCounter() shows the filtering is
happening on shredded files
- cutting back on category count and attempting to change structure of file
Note: this is the benchmark branch, and has had the assertions on counters and counter reset
cut; all the other wiring up for the assertions is present.
Change-Id: I686be12d51b13d2048b631b8cf198651012cc474
Allows for assertions in tests and in benchmarks that rowgroup skipping is taking place.
Needed as there's not much tangible speedup, yet
Change-Id: I8c03eb33d2d3d8a2139c347e6a72a7284e627f62
...which shows the configuration changes needed for data to be saved to multiple rowgroups.
File size shrunk; increasing iterations of runs.
Change-Id: Icb622958a068ed67de5bb895d88a9aa1713d2b11
- increasing category count reduces # of matches on the single category, so amplifying shredding filtering advantage
- and varcat select with a range > 0 and < 1. That's the same as the = 1 and `in (1)`, selections, but with two scans of the values.
Change-Id: Ib2b139697e235cb4674503784c6c909a5c460d1a
- fork=1, repetitions=5
- review feedback: use bigint in variant_get() on long values.
Remove all commented out code referring to the predicate pushdown;
everything is lined up waiting for that PR.
@steveloughran

steveloughran commented May 28, 2026

Copy link
Copy Markdown
ContributorAuthor

Marking as ready for review to the extent that the current test dataset shows speedup when ran atop the separate PRs needed for predicate pushdown on shredded variant fields.

here each sql query is run with avro, unshredded and shredded variants:
Screenshot 2026-05-28 at 19 08 52

before: note how shredded is pathologically worse
Screenshot 2026-05-28 at 19 06 48

after: note how shredded variants are faster than unshredded for filtering operations.

Screenshot 2026-05-28 at 19 08 15

Special highlight for filtering on an integer field where the value is out of range or not in the IN membership of a query. These only examine the metrics and can skip reading any of the values.

Note: don't compare the actual numbers in the two runs, that's too rigorous. Key is that the benchmark shows that when predicate pushdown works, the queries are faster. That is, the dataset and queries of this benchmark are sufficient to show this.

The pushdown work adds some package private counters to allow tests to see if range checks took place, and if the results declared that rowgroups could be skipped.
This benchmark is in the package to support this and has assertions through the benchmarks to verify skipping is occurring when expected.
The actual probes, and per-run resetting of counters, are cut from this pr; they will need to be re-inserted

@steveloughran
steveloughran marked this pull request as ready for review May 28, 2026 18:16
@steveloughran

Copy link
Copy Markdown
ContributorAuthor

@nssalian what do you think?

@steveloughran

Copy link
Copy Markdown
ContributorAuthor

@rdblue this pr is ready for review in that "the data it generates, the layout and the queries are sufficient to show tangible speedup with qlong's file skipping and my predicate pushdown changes"
If you have suggestions, I can get back to them from June 22.

@steveloughran

Copy link
Copy Markdown
ContributorAuthor

still ready for review

@steveloughran

Copy link
Copy Markdown
ContributorAuthor

@Fokko this is ready for its final look-through; i've rebased it but only the final two commits are new, one for the javadocs and the final one with real code changes

@nssalian what do you think here?

}

/** Log the parquet metrics invocations. */
private void logRowGroupFiltering() {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

logRowGroupFiltering(), resetMetricsCounter(), and expectFilteringOfShreddedFields() are all empty here but their javadoc describes behavior that doesn't exist, and expectPushdownWhenShredded is plumbed through select/selectEmptyResults just to gate the no-op expectFilteringOfShreddedFields(). Could these either be implemented or removed (along with the expectPushdownWhenShredded param)? Leaving them empty is a bit misleading to the next reader.

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.

ok, I will cut and when pushdown is added, they can be reinstated

* <p>TODO: failing in spark. Needs investigation.
*/
// @Benchmark
public void joinArrElt4OnVarcat(Blackhole blackhole) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we drop the commented-out joinArrElt4OnVarcat and open an issue for the failing self-join instead? A linked issue tracks it better than a // @Benchmark + TODO.

/** Number of distinct category values. */
private static final int NUM_CATEGORIES = 10;

public static final String COL_ID = "id";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: COL_ID and EQUALITY_CHECK are public while COL_NESTED/COL_ARR right below are private, and the sibling benchmarks keep their constants private. Same for TableType (and Depth/DEEP_NEST_DEPTH/ITERATIONS in the core benchmark). Worth making these private to match.

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.

I'll make them all private,inevitably just ide refactoring setup

@steveloughransteveloughran left a comment

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.

@nssalian I will address your comments. Now that spark 4.2 is out we can make a bit more progress with variants, though I will have to move this pr onto being onto the spark/v4.2/ path as soon as it is added.

w.r.t the empty assertions, I will cut. for the curious here are the full ones

 /** Log the parquet metrics invocations. */
private void logRowGroupFiltering() {
final long scans = ParquetMetricsRowGroupFilter.variantPredicatesShreddedMetricsEvaluated();
final long skipped = ParquetMetricsRowGroupFilter.variantPredicatesShreddedSkipped();
LOG.info("Scanned {} shredded metrics, skipped {} row groups", scans, skipped);
}
/** Reset the metrics counter. */
private void resetMetricsCounter() {
ParquetMetricsRowGroupFilter.resetShreddedMetricsCounters();
}
/**
* On shredded tables, assert that shredded field were scanned for filtering operations. Log the
* count at info.
*/
private void expectFilteringOfShreddedFields() {
if (isShredded()) {
final long scans = ParquetMetricsRowGroupFilter.variantPredicatesShreddedMetricsEvaluated();
final long skipped = ParquetMetricsRowGroupFilter.variantPredicatesShreddedSkipped();
LOG.info("Scanned {} shredded metrics, skipped {} row groups", scans, skipped);
assertThat(scans)
.describedAs("Number of times rowgroup metrics of shredded fields were scanned")
.isGreaterThan(0);
assertThat(skipped).describedAs("rowgroups skipped").isGreaterThan(0);
}
}

+ review comments.
Change-Id: I44a5d6815cb8113bdf8ada8645089f859807f7bf
@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions.

@steveloughran

Copy link
Copy Markdown
ContributorAuthor

@manuzhang thanks for marking as not stale. I am now ex cloudera, laptopless and mostly restricting my coding to learning golang for the fun of it right now. Maybe I'll get set up for java dev next week.

@nssalian

Copy link
Copy Markdown
Collaborator

@steveloughran it might be hard to get this in this release but I can try doing this after if you don't get a chance.

@steveloughran

Copy link
Copy Markdown
ContributorAuthor

I'm not set up to do any java dev right now, so lets target the next release. The big source of merge pain will be the move to spark 4.2; when that goes in the pr will need the spark oart moved, but it will be straightforward

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.

Core, Spark: Add JMH benchmarks for Variants

5 participants

@steveloughran@nssalian@rdblue@manuzhang@rashworld-max