Files
goldenChart/ta4j-master/ta4j-examples/src/main/java/ta4jexamples/rules/RuleNameBenchmark.java
T
2026-05-23 15:11:48 +09:00

240 lines
9.9 KiB
Java

/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.rules;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ta4j.core.Rule;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.rules.AbstractRule;
import org.ta4j.core.rules.AndRule;
import org.ta4j.core.rules.FixedRule;
/**
* Comparative benchmark for eager vs lazy rule naming.
* <p>
* Scenarios:
* <ul>
* <li>Eagerly setting AndRule's name in constructor and then calling getName()
* (current default)</li>
* <li>Lazily setting/getting AndRule's name in getName() (i.e. don't set name
* in constructor)</li>
* <li>Eagerly setting the name in constructor (using class names, not calling
* getName() on children) but never calling getName() - demonstrates that JIT
* may optimize away string construction overhead when the name is never
* accessed, though the volatile write itself cannot be eliminated</li>
* </ul>
*
* @since 0.22.0
*/
public class RuleNameBenchmark {
private static final Logger LOG = LogManager.getLogger(RuleNameBenchmark.class);
private static final int DEFAULT_THREADS = Math.max(8, Runtime.getRuntime().availableProcessors());
private static final int DEFAULT_RULES_PER_THREAD = 100_000;
private static final int DEFAULT_BATCHES = 3;
public static void main(String[] args) throws Exception {
int threads = args.length > 0 ? Integer.parseInt(args[0]) : DEFAULT_THREADS;
int batches = args.length > 1 ? Integer.parseInt(args[1]) : DEFAULT_BATCHES;
int rulesPerThread = args.length > 2 ? Integer.parseInt(args[2]) : DEFAULT_RULES_PER_THREAD;
new RuleNameBenchmark().run(threads, batches, rulesPerThread);
}
private void run(int threads, int batches, int rulesPerThread) throws Exception {
String formattedRulesPerThread = NumberFormat.getIntegerInstance(Locale.US).format(rulesPerThread);
LOG.info("Starting rule construction benchmark: threads={}, batches={}, rulesPerThread={}", threads, batches,
formattedRulesPerThread);
Map<String, ScenarioStats> statsByScenario = new HashMap<>();
for (int batch = 1; batch <= batches; batch++) {
runScenario("Eager setName() + getName()", threads, rulesPerThread, this::buildEagerRule, true, batch,
statsByScenario);
runScenario("Lazy setName() + getName()", threads, rulesPerThread, this::buildLazyRule, true, batch,
statsByScenario);
runScenario("Eager setName() + No getName()", threads, rulesPerThread, this::buildEagerRuleNoChildNames,
false, batch, statsByScenario);
}
LOG.info("=== Rule construction throughput summary (threads={}, batches={}, rulesPerThread={}) ===", threads,
batches, formattedRulesPerThread);
statsByScenario.forEach((label, s) -> {
double avgThroughput = s.totalThroughput / s.runs;
Duration avgDuration = Duration.ofNanos((long) (s.totalDurationNanos / s.runs));
LOG.info(
"{}: runs={}, avgThroughput={} rules/s, minThroughput={}, maxThroughput={}, avgDuration={}, totalChecksum={}",
label, s.runs, NumberFormat.getIntegerInstance(Locale.US).format(avgThroughput),
NumberFormat.getIntegerInstance(Locale.US).format(s.minThroughput),
NumberFormat.getIntegerInstance(Locale.US).format(s.maxThroughput), avgDuration, s.totalChecksum);
});
}
private void runScenario(String label, int threads, int rulesPerThread, Supplier<Rule> factory, boolean callName,
int batch, Map<String, ScenarioStats> statsByScenario) throws Exception {
long started = System.nanoTime();
long checksum = exercise(threads, rulesPerThread, factory, callName);
long elapsedNanos = System.nanoTime() - started;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0;
double constructions = (double) threads * rulesPerThread;
double throughput = constructions / elapsedSeconds;
LOG.debug("Batch {} [{}]: duration={}, throughput={} rules/s, checksum={}", batch, label,
Duration.ofNanos(elapsedNanos), NumberFormat.getIntegerInstance(Locale.US).format(throughput),
checksum);
ScenarioStats stats = statsByScenario.computeIfAbsent(label, k -> new ScenarioStats());
stats.runs++;
stats.totalDurationNanos += elapsedNanos;
stats.totalThroughput += throughput;
stats.totalChecksum += checksum;
stats.minThroughput = Math.min(stats.minThroughput, throughput);
stats.maxThroughput = Math.max(stats.maxThroughput, throughput);
}
private long exercise(int threads, int rulesPerThread, Supplier<Rule> factory, boolean callName) throws Exception {
var pool = Executors.newFixedThreadPool(threads);
try {
List<CompletableFuture<Long>> futures = new ArrayList<>(threads);
for (int t = 0; t < threads; t++) {
futures.add(CompletableFuture.supplyAsync(() -> {
long localChecksum = 0;
for (int i = 0; i < rulesPerThread; i++) {
Rule r = factory.get();
if (callName) {
String name = r.getName();
localChecksum += name.hashCode();
} else {
// Touch type to prevent dead-code elimination without invoking getName()
localChecksum += r.getClass().hashCode();
}
}
return localChecksum;
}, pool));
}
long total = 0;
for (CompletableFuture<Long> future : futures) {
total += future.get(5, TimeUnit.MINUTES);
}
return total;
} finally {
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);
}
}
private Rule buildEagerRule() {
Rule left = new FixedRule(1);
Rule right = new FixedRule(2);
return new AndRule(left, right);
}
Rule buildLazyRule() {
Rule left = new FixedRule(1);
Rule right = new FixedRule(2);
return new LazyAndRule(left, right);
}
Rule buildEagerRuleNoChildNames() {
Rule left = new FixedRule(1);
Rule right = new FixedRule(2);
return new NoNameLeakAndRule(left, right);
}
/**
* Lazy variant of AndRule that defers name construction to getName().
*/
static final class LazyAndRule extends AbstractRule {
private final Rule rule1;
private final Rule rule2;
LazyAndRule(Rule rule1, Rule rule2) {
this.rule1 = rule1;
this.rule2 = rule2;
}
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
boolean satisfied = rule1.isSatisfied(index, tradingRecord) && rule2.isSatisfied(index, tradingRecord);
traceIsSatisfied(index, satisfied);
return satisfied;
}
@Override
protected String createDefaultName() {
setName(createCompositeName(getClass().getSimpleName(), rule1, rule2));
return getName();
}
}
/**
* Eager variant that constructs the name in the constructor but avoids calling
* getName() on children (to prevent triggering their lazy name resolution).
* This demonstrates that even when setName() is called eagerly, if getName() is
* never invoked on the rule, the JIT compiler may be able to optimize away some
* of the string construction overhead, though the volatile write itself cannot
* be eliminated.
* <p>
* Note: The volatile write in setName() is an observable side effect that
* prevents complete dead-code elimination, but the string construction work
* (StringBuilder operations) may still be optimized if the result is never
* read.
*/
static final class NoNameLeakAndRule extends AbstractRule {
private final Rule rule1;
private final Rule rule2;
NoNameLeakAndRule(Rule rule1, Rule rule2) {
this.rule1 = rule1;
this.rule2 = rule2;
// Eagerly construct and set the name, but use class names instead of
// calling getName() on children to avoid triggering their lazy name resolution.
// This allows us to test if JIT can eliminate the string construction
// overhead when the name is never accessed.
setName(buildNameWithoutChildNames());
}
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
boolean satisfied = rule1.isSatisfied(index, tradingRecord) && rule2.isSatisfied(index, tradingRecord);
traceIsSatisfied(index, satisfied);
return satisfied;
}
private String buildNameWithoutChildNames() {
StringBuilder builder = new StringBuilder(getClass().getSimpleName());
builder.append('(');
builder.append(rule1 == null ? "null" : rule1.getClass().getSimpleName());
builder.append(',');
builder.append(rule2 == null ? "null" : rule2.getClass().getSimpleName());
builder.append(')');
return builder.toString();
}
}
private static final class ScenarioStats {
int runs;
double totalDurationNanos;
double totalThroughput;
double minThroughput = Double.MAX_VALUE;
double maxThroughput = Double.MIN_VALUE;
long totalChecksum;
}
}