diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 181a0fbcf..d63d9aec5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -154,6 +154,7 @@ set(yul_phaser_sources yulPhaser/Phaser.cpp yulPhaser/Population.cpp yulPhaser/Program.cpp + yulPhaser/ProgramCache.cpp yulPhaser/Selections.cpp yulPhaser/SimulationRNG.cpp @@ -170,6 +171,7 @@ set(yul_phaser_sources ../tools/yulPhaser/Phaser.cpp ../tools/yulPhaser/Population.cpp ../tools/yulPhaser/Program.cpp + ../tools/yulPhaser/ProgramCache.cpp ../tools/yulPhaser/Selections.cpp ../tools/yulPhaser/SimulationRNG.cpp ) diff --git a/test/yulPhaser/AlgorithmRunner.cpp b/test/yulPhaser/AlgorithmRunner.cpp index 0b54ad38f..0382cc90d 100644 --- a/test/yulPhaser/AlgorithmRunner.cpp +++ b/test/yulPhaser/AlgorithmRunner.cpp @@ -20,6 +20,8 @@ #include #include +#include + #include #include @@ -29,6 +31,7 @@ using namespace std; using namespace boost::unit_test::framework; using namespace boost::test_tools; +using namespace solidity::langutil; using namespace solidity::util; namespace fs = boost::filesystem; @@ -92,7 +95,7 @@ BOOST_AUTO_TEST_SUITE(AlgorithmRunnerTest) BOOST_FIXTURE_TEST_CASE(run_should_call_runNextRound_once_per_round, AlgorithmRunnerFixture) { m_options.maxRounds = 5; - AlgorithmRunner runner(Population(m_fitnessMetric), m_options, m_output); + AlgorithmRunner runner(Population(m_fitnessMetric), {}, m_options, m_output); CountingAlgorithm algorithm; @@ -112,6 +115,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_print_the_top_chromosome, AlgorithmRunnerFixt // NOTE: Chromosomes chosen so that they're not substrings of each other and are not // words likely to appear in the output in normal circumstances. Population(m_fitnessMetric, {Chromosome("fcCUnDve"), Chromosome("jsxIOo"), Chromosome("ighTLM")}), + {}, m_options, m_output ); @@ -131,7 +135,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_save_initial_population_to_file_if_autosave_f { m_options.maxRounds = 0; m_options.populationAutosaveFile = m_autosavePath; - AlgorithmRunner runner(m_population, m_options, m_output); + AlgorithmRunner runner(m_population, {}, m_options, m_output); assert(!fs::exists(m_autosavePath)); runner.run(m_algorithm); @@ -145,7 +149,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_save_population_to_file_if_autosave_file_spec { m_options.maxRounds = 1; m_options.populationAutosaveFile = m_autosavePath; - AlgorithmRunner runner(m_population, m_options, m_output); + AlgorithmRunner runner(m_population, {}, m_options, m_output); assert(!fs::exists(m_autosavePath)); runner.run(m_algorithm); @@ -159,7 +163,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_overwrite_existing_file_if_autosave_file_spec { m_options.maxRounds = 5; m_options.populationAutosaveFile = m_autosavePath; - AlgorithmRunner runner(m_population, m_options, m_output); + AlgorithmRunner runner(m_population, {}, m_options, m_output); assert(!fs::exists(m_autosavePath)); vector originalContent = {"Original content"}; @@ -180,7 +184,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_not_save_population_to_file_if_autosave_file_ { m_options.maxRounds = 5; m_options.populationAutosaveFile = nullopt; - AlgorithmRunner runner(m_population, m_options, m_output); + AlgorithmRunner runner(m_population, {}, m_options, m_output); assert(!fs::exists(m_autosavePath)); runner.run(m_algorithm); @@ -198,7 +202,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_randomise_duplicate_chromosomes_if_requested, m_options.randomiseDuplicates = true; m_options.minChromosomeLength = 50; m_options.maxChromosomeLength = 50; - AlgorithmRunner runner(population, m_options, m_output); + AlgorithmRunner runner(population, {}, m_options, m_output); runner.run(algorithm); @@ -227,7 +231,7 @@ BOOST_FIXTURE_TEST_CASE(run_should_not_randomise_duplicate_chromosomes_if_not_re m_options.maxRounds = 1; m_options.randomiseDuplicates = false; - AlgorithmRunner runner(population, m_options, m_output); + AlgorithmRunner runner(population, {}, m_options, m_output); runner.run(algorithm); @@ -237,6 +241,33 @@ BOOST_FIXTURE_TEST_CASE(run_should_not_randomise_duplicate_chromosomes_if_not_re BOOST_TEST(runner.population().individuals()[2].chromosome == duplicate); } +BOOST_FIXTURE_TEST_CASE(run_should_clear_cache_at_the_beginning_and_update_it_before_each_round, AlgorithmRunnerFixture) +{ + CharStream sourceStream = CharStream("{}", current_test_case().p_name); + vector> caches = { + make_shared(get(Program::load(sourceStream))), + make_shared(get(Program::load(sourceStream))), + }; + + m_options.maxRounds = 10; + AlgorithmRunner runner(Population(m_fitnessMetric), caches, m_options, m_output); + CountingAlgorithm algorithm; + + BOOST_TEST(algorithm.m_currentRound == 0); + BOOST_TEST(caches[0]->currentRound() == 0); + BOOST_TEST(caches[1]->currentRound() == 0); + + runner.run(algorithm); + BOOST_TEST(algorithm.m_currentRound == 10); + BOOST_TEST(caches[0]->currentRound() == 10); + BOOST_TEST(caches[1]->currentRound() == 10); + + runner.run(algorithm); + BOOST_TEST(algorithm.m_currentRound == 20); + BOOST_TEST(caches[0]->currentRound() == 10); + BOOST_TEST(caches[1]->currentRound() == 10); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/test/yulPhaser/FitnessMetrics.cpp b/test/yulPhaser/FitnessMetrics.cpp index 8b62b03b7..524ef6357 100644 --- a/test/yulPhaser/FitnessMetrics.cpp +++ b/test/yulPhaser/FitnessMetrics.cpp @@ -76,15 +76,16 @@ protected: Chromosome m_chromosome{vector{UnusedPruner::name, EquivalentFunctionCombiner::name}}; Program m_program = get(Program::load(m_sourceStream)); Program m_optimisedProgram = optimisedProgram(m_program); + shared_ptr m_programCache = make_shared(m_program); }; class FitnessMetricCombinationFixture: public ProgramBasedMetricFixture { protected: vector> m_simpleMetrics = { - make_shared(m_program, 1), - make_shared(m_program, 2), - make_shared(m_program, 3), + make_shared(m_program, nullptr, 1), + make_shared(m_program, nullptr, 2), + make_shared(m_program, nullptr, 3), }; vector m_fitness = { m_simpleMetrics[0]->evaluate(m_chromosome), @@ -97,31 +98,66 @@ BOOST_AUTO_TEST_SUITE(Phaser) BOOST_AUTO_TEST_SUITE(FitnessMetricsTest) BOOST_AUTO_TEST_SUITE(ProgramBasedMetricTest) -BOOST_FIXTURE_TEST_CASE(optimisedProgram_should_return_optimised_program, ProgramBasedMetricFixture) +BOOST_FIXTURE_TEST_CASE(optimisedProgram_should_return_optimised_program_even_if_cache_not_available, ProgramBasedMetricFixture) { - string code = toString(DummyProgramBasedMetric(m_program).optimisedProgram(m_chromosome)); + string code = toString(DummyProgramBasedMetric(m_program, nullptr).optimisedProgram(m_chromosome)); BOOST_TEST(code != toString(m_program)); BOOST_TEST(code == toString(m_optimisedProgram)); } +BOOST_FIXTURE_TEST_CASE(optimisedProgram_should_use_cache_if_available, ProgramBasedMetricFixture) +{ + string code = toString(DummyProgramBasedMetric(nullopt, m_programCache).optimisedProgram(m_chromosome)); + + BOOST_TEST(code != toString(m_program)); + BOOST_TEST(code == toString(m_optimisedProgram)); + BOOST_TEST(m_programCache->size() == m_chromosome.length()); +} + +BOOST_FIXTURE_TEST_CASE(optimisedProgramNoCache_should_return_optimised_program_even_if_cache_not_available, ProgramBasedMetricFixture) +{ + string code = toString(DummyProgramBasedMetric(m_program, nullptr).optimisedProgramNoCache(m_chromosome)); + + BOOST_TEST(code != toString(m_program)); + BOOST_TEST(code == toString(m_optimisedProgram)); +} + +BOOST_FIXTURE_TEST_CASE(optimisedProgramNoCache_should_not_use_cache_even_if_available, ProgramBasedMetricFixture) +{ + string code = toString(DummyProgramBasedMetric(nullopt, m_programCache).optimisedProgramNoCache(m_chromosome)); + + BOOST_TEST(code != toString(m_program)); + BOOST_TEST(code == toString(m_optimisedProgram)); + BOOST_TEST(m_programCache->size() == 0); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(ProgramSizeTest) BOOST_FIXTURE_TEST_CASE(evaluate_should_compute_size_of_the_optimised_program, ProgramBasedMetricFixture) { - size_t fitness = ProgramSize(m_program).evaluate(m_chromosome); + size_t fitness = ProgramSize(m_program, nullptr).evaluate(m_chromosome); BOOST_TEST(fitness != m_program.codeSize()); BOOST_TEST(fitness == m_optimisedProgram.codeSize()); } +BOOST_FIXTURE_TEST_CASE(evaluate_should_be_able_to_use_program_cache_if_available, ProgramBasedMetricFixture) +{ + size_t fitness = ProgramSize(nullopt, m_programCache).evaluate(m_chromosome); + + BOOST_TEST(fitness != m_program.codeSize()); + BOOST_TEST(fitness == m_optimisedProgram.codeSize()); + BOOST_TEST(m_programCache->size() == m_chromosome.length()); +} + BOOST_FIXTURE_TEST_CASE(evaluate_should_repeat_the_optimisation_specified_number_of_times, ProgramBasedMetricFixture) { Program const& programOptimisedOnce = m_optimisedProgram; Program programOptimisedTwice = optimisedProgram(programOptimisedOnce); - ProgramSize metric(m_program, 2); + ProgramSize metric(m_program, nullptr, 2); size_t fitness = metric.evaluate(m_chromosome); BOOST_TEST(fitness != m_program.codeSize()); @@ -131,7 +167,7 @@ BOOST_FIXTURE_TEST_CASE(evaluate_should_repeat_the_optimisation_specified_number BOOST_FIXTURE_TEST_CASE(evaluate_should_not_optimise_if_number_of_repetitions_is_zero, ProgramBasedMetricFixture) { - ProgramSize metric(m_program, 0); + ProgramSize metric(m_program, nullptr, 0); size_t fitness = metric.evaluate(m_chromosome); BOOST_TEST(fitness == m_program.codeSize()); @@ -143,7 +179,13 @@ BOOST_AUTO_TEST_SUITE(RelativeProgramSizeTest) BOOST_FIXTURE_TEST_CASE(evaluate_should_compute_the_size_ratio_between_optimised_program_and_original_program, ProgramBasedMetricFixture) { - BOOST_TEST(RelativeProgramSize(m_program, 3).evaluate(m_chromosome) == round(1000.0 * m_optimisedProgram.codeSize() / m_program.codeSize())); + BOOST_TEST(RelativeProgramSize(m_program, nullptr, 3).evaluate(m_chromosome) == round(1000.0 * m_optimisedProgram.codeSize() / m_program.codeSize())); +} + +BOOST_FIXTURE_TEST_CASE(evaluate_should_be_able_to_use_program_cache_if_available, ProgramBasedMetricFixture) +{ + BOOST_TEST(RelativeProgramSize(nullopt, m_programCache, 3).evaluate(m_chromosome) == round(1000.0 * m_optimisedProgram.codeSize() / m_program.codeSize())); + BOOST_TEST(m_programCache->size() == m_chromosome.length()); } BOOST_FIXTURE_TEST_CASE(evaluate_should_repeat_the_optimisation_specified_number_of_times, ProgramBasedMetricFixture) @@ -151,17 +193,17 @@ BOOST_FIXTURE_TEST_CASE(evaluate_should_repeat_the_optimisation_specified_number Program const& programOptimisedOnce = m_optimisedProgram; Program programOptimisedTwice = optimisedProgram(programOptimisedOnce); - RelativeProgramSize metric(m_program, 3, 2); + RelativeProgramSize metric(m_program, nullptr, 3, 2); size_t fitness = metric.evaluate(m_chromosome); BOOST_TEST(fitness != 1000); - BOOST_TEST(fitness != RelativeProgramSize(programOptimisedTwice, 3, 1).evaluate(m_chromosome)); + BOOST_TEST(fitness != RelativeProgramSize(programOptimisedTwice, nullptr, 3, 1).evaluate(m_chromosome)); BOOST_TEST(fitness == round(1000.0 * programOptimisedTwice.codeSize() / m_program.codeSize())); } BOOST_FIXTURE_TEST_CASE(evaluate_should_return_one_if_number_of_repetitions_is_zero, ProgramBasedMetricFixture) { - RelativeProgramSize metric(m_program, 3, 0); + RelativeProgramSize metric(m_program, nullptr, 3, 0); BOOST_TEST(metric.evaluate(m_chromosome) == 1000); } @@ -171,7 +213,7 @@ BOOST_FIXTURE_TEST_CASE(evaluate_should_return_one_if_the_original_program_size_ CharStream sourceStream = CharStream("{}", ""); Program program = get(Program::load(sourceStream)); - RelativeProgramSize metric(program, 3); + RelativeProgramSize metric(program, nullptr, 3); BOOST_TEST(metric.evaluate(m_chromosome) == 1000); BOOST_TEST(metric.evaluate(Chromosome("")) == 1000); @@ -181,11 +223,11 @@ BOOST_FIXTURE_TEST_CASE(evaluate_should_return_one_if_the_original_program_size_ BOOST_FIXTURE_TEST_CASE(evaluate_should_multiply_the_result_by_scaling_factor, ProgramBasedMetricFixture) { double sizeRatio = static_cast(m_optimisedProgram.codeSize()) / m_program.codeSize(); - BOOST_TEST(RelativeProgramSize(m_program, 0).evaluate(m_chromosome) == round(1.0 * sizeRatio)); - BOOST_TEST(RelativeProgramSize(m_program, 1).evaluate(m_chromosome) == round(10.0 * sizeRatio)); - BOOST_TEST(RelativeProgramSize(m_program, 2).evaluate(m_chromosome) == round(100.0 * sizeRatio)); - BOOST_TEST(RelativeProgramSize(m_program, 3).evaluate(m_chromosome) == round(1000.0 * sizeRatio)); - BOOST_TEST(RelativeProgramSize(m_program, 4).evaluate(m_chromosome) == round(10000.0 * sizeRatio)); + BOOST_TEST(RelativeProgramSize(m_program, nullptr, 0).evaluate(m_chromosome) == round(1.0 * sizeRatio)); + BOOST_TEST(RelativeProgramSize(m_program, nullptr, 1).evaluate(m_chromosome) == round(10.0 * sizeRatio)); + BOOST_TEST(RelativeProgramSize(m_program, nullptr, 2).evaluate(m_chromosome) == round(100.0 * sizeRatio)); + BOOST_TEST(RelativeProgramSize(m_program, nullptr, 3).evaluate(m_chromosome) == round(1000.0 * sizeRatio)); + BOOST_TEST(RelativeProgramSize(m_program, nullptr, 4).evaluate(m_chromosome) == round(10000.0 * sizeRatio)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/yulPhaser/Phaser.cpp b/test/yulPhaser/Phaser.cpp index 04dfbe3dc..633462865 100644 --- a/test/yulPhaser/Phaser.cpp +++ b/test/yulPhaser/Phaser.cpp @@ -55,7 +55,7 @@ protected: }; }; -class FitnessMetricFactoryFixture +class FixtureWithPrograms { protected: vector m_sourceStreams = { @@ -68,6 +68,11 @@ protected: get(Program::load(m_sourceStreams[1])), get(Program::load(m_sourceStreams[2])), }; +}; + +class FitnessMetricFactoryFixture: public FixtureWithPrograms +{ +protected: FitnessMetricFactory::Options m_options = { /* metric = */ MetricChoice::CodeSize, /* metricAggregator = */ MetricAggregatorChoice::Average, @@ -154,7 +159,7 @@ BOOST_FIXTURE_TEST_CASE(build_should_create_metric_of_the_right_type, FitnessMet { m_options.metric = MetricChoice::RelativeCodeSize; m_options.metricAggregator = MetricAggregatorChoice::Sum; - unique_ptr metric = FitnessMetricFactory::build(m_options, {m_programs[0]}); + unique_ptr metric = FitnessMetricFactory::build(m_options, {m_programs[0]}, {nullptr}); BOOST_REQUIRE(metric != nullptr); auto sumMetric = dynamic_cast(metric.get()); @@ -172,7 +177,7 @@ BOOST_FIXTURE_TEST_CASE(build_should_respect_chromosome_repetitions_option, Fitn m_options.metric = MetricChoice::CodeSize; m_options.metricAggregator = MetricAggregatorChoice::Average; m_options.chromosomeRepetitions = 5; - unique_ptr metric = FitnessMetricFactory::build(m_options, {m_programs[0]}); + unique_ptr metric = FitnessMetricFactory::build(m_options, {m_programs[0]}, {nullptr}); BOOST_REQUIRE(metric != nullptr); auto averageMetric = dynamic_cast(metric.get()); @@ -190,7 +195,7 @@ BOOST_FIXTURE_TEST_CASE(build_should_set_relative_metric_scale, FitnessMetricFac m_options.metric = MetricChoice::RelativeCodeSize; m_options.metricAggregator = MetricAggregatorChoice::Average; m_options.relativeMetricScale = 10; - unique_ptr metric = FitnessMetricFactory::build(m_options, {m_programs[0]}); + unique_ptr metric = FitnessMetricFactory::build(m_options, {m_programs[0]}, {nullptr}); BOOST_REQUIRE(metric != nullptr); auto averageMetric = dynamic_cast(metric.get()); @@ -205,7 +210,11 @@ BOOST_FIXTURE_TEST_CASE(build_should_set_relative_metric_scale, FitnessMetricFac BOOST_FIXTURE_TEST_CASE(build_should_create_metric_for_each_input_program, FitnessMetricFactoryFixture) { - unique_ptr metric = FitnessMetricFactory::build(m_options, m_programs); + unique_ptr metric = FitnessMetricFactory::build( + m_options, + m_programs, + vector>(m_programs.size(), nullptr) + ); BOOST_REQUIRE(metric != nullptr); auto combinedMetric = dynamic_cast(metric.get()); @@ -213,6 +222,31 @@ BOOST_FIXTURE_TEST_CASE(build_should_create_metric_for_each_input_program, Fitne BOOST_REQUIRE(combinedMetric->metrics().size() == m_programs.size()); } +BOOST_FIXTURE_TEST_CASE(build_should_pass_program_caches_to_metrics, FitnessMetricFactoryFixture) +{ + assert(m_programs.size() == 3); + vector> caches = { + make_shared(m_programs[0]), + make_shared(m_programs[1]), + make_shared(m_programs[2]), + }; + + m_options.metric = MetricChoice::RelativeCodeSize; + unique_ptr metric = FitnessMetricFactory::build(m_options, m_programs, caches); + BOOST_REQUIRE(metric != nullptr); + + auto combinedMetric = dynamic_cast(metric.get()); + BOOST_REQUIRE(combinedMetric != nullptr); + BOOST_REQUIRE(combinedMetric->metrics().size() == caches.size()); + + for (size_t i = 0; i < caches.size(); ++i) + { + auto programBasedMetric = dynamic_cast(combinedMetric->metrics()[i].get()); + BOOST_REQUIRE(programBasedMetric != nullptr); + BOOST_TEST(programBasedMetric->programCache() == caches[i].get()); + } +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(PopulationFactoryTest) @@ -317,6 +351,35 @@ BOOST_FIXTURE_TEST_CASE(build_should_combine_populations_from_all_sources, Poula BOOST_TEST(count(begin, end, Individual(Chromosome("fcL"), *m_fitnessMetric)) >= 2); } + +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(ProgramCacheFactoryTest) + +BOOST_FIXTURE_TEST_CASE(build_should_create_cache_for_each_input_program_if_cache_enabled, FixtureWithPrograms) +{ + ProgramCacheFactory::Options options{/* programCacheEnabled = */ true}; + vector> caches = ProgramCacheFactory::build(options, m_programs); + assert(m_programs.size() >= 2 && "There must be at least 2 programs for this test to be meaningful"); + + BOOST_TEST(caches.size() == m_programs.size()); + for (size_t i = 0; i < m_programs.size(); ++i) + { + BOOST_REQUIRE(caches[i] != nullptr); + BOOST_TEST(toString(caches[i]->program()) == toString(m_programs[i])); + } +} + +BOOST_FIXTURE_TEST_CASE(build_should_return_nullptr_for_each_input_program_if_cache_disabled, FixtureWithPrograms) +{ + ProgramCacheFactory::Options options{/* programCacheEnabled = */ false}; + vector> caches = ProgramCacheFactory::build(options, m_programs); + assert(m_programs.size() >= 2 && "There must be at least 2 programs for this test to be meaningful"); + + BOOST_TEST(caches.size() == m_programs.size()); + for (size_t i = 0; i < m_programs.size(); ++i) + BOOST_TEST(caches[i] == nullptr); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(ProgramFactoryTest) diff --git a/test/yulPhaser/ProgramCache.cpp b/test/yulPhaser/ProgramCache.cpp new file mode 100644 index 000000000..598e7a16f --- /dev/null +++ b/test/yulPhaser/ProgramCache.cpp @@ -0,0 +1,207 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +#include +#include + +#include + +#include + +#include + +#include +#include + +using namespace std; +using namespace solidity::util; +using namespace solidity::langutil; +using namespace solidity::yul; + +namespace solidity::phaser::test +{ + +class ProgramCacheFixture +{ +protected: + static constexpr char SampleSourceCode[] = + "{\n" + " for { let i := 0 } not(eq(i, 15)) { i := add(i, 1) }\n" + " {\n" + " let x := 1\n" + " mstore(i, 2)\n" + " }\n" + "}\n"; + + Program optimisedProgram(Program _program, string _abbreviatedOptimisationSteps) const + { + Program result = move(_program); + result.optimise(Chromosome(_abbreviatedOptimisationSteps).optimisationSteps()); + return result; + } + + static set cachedKeys(ProgramCache const& _programCache) + { + set keys; + for (auto pair = _programCache.entries().begin(); pair != _programCache.entries().end(); ++pair) + keys.insert(pair->first); + + return keys; + } + + CharStream m_sourceStream = CharStream(SampleSourceCode, "program-cache-test"); + Program m_program = get(Program::load(m_sourceStream)); + ProgramCache m_programCache{m_program}; +}; + +BOOST_AUTO_TEST_SUITE(Phaser) +BOOST_AUTO_TEST_SUITE(ProgramCacheTest) + +BOOST_FIXTURE_TEST_CASE(optimiseProgram_should_apply_optimisation_steps_to_program, ProgramCacheFixture) +{ + Program expectedProgram = optimisedProgram(m_program, "IuO"); + assert(toString(expectedProgram) != toString(m_program)); + + Program cachedProgram = m_programCache.optimiseProgram("IuO"); + + BOOST_TEST(toString(cachedProgram) == toString(expectedProgram)); +} + +BOOST_FIXTURE_TEST_CASE(optimiseProgram_should_store_programs_for_all_prefixes, ProgramCacheFixture) +{ + Program programI = optimisedProgram(m_program, "I"); + Program programIu = optimisedProgram(programI, "u"); + Program programIuO = optimisedProgram(programIu, "O"); + assert(toString(m_program) != toString(programI)); + assert(toString(m_program) != toString(programIu)); + assert(toString(m_program) != toString(programIuO)); + assert(toString(programI) != toString(programIu)); + assert(toString(programI) != toString(programIuO)); + assert(toString(programIu) != toString(programIuO)); + + BOOST_REQUIRE(m_programCache.size() == 0); + + Program cachedProgram = m_programCache.optimiseProgram("IuO"); + + BOOST_TEST(toString(cachedProgram) == toString(programIuO)); + + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu", "IuO"})); + BOOST_TEST(toString(*m_programCache.find("I")) == toString(programI)); + BOOST_TEST(toString(*m_programCache.find("Iu")) == toString(programIu)); + BOOST_TEST(toString(*m_programCache.find("IuO")) == toString(programIuO)); +} + +BOOST_FIXTURE_TEST_CASE(optimiseProgram_should_repeat_the_chromosome_requested_number_of_times, ProgramCacheFixture) +{ + string steps = "IuOIuO"; + + Program cachedProgram = m_programCache.optimiseProgram("IuO", 2); + + ProgramCache cacheNoRepetitions(m_program); + Program cachedProgramNoRepetitions = cacheNoRepetitions.optimiseProgram("IuOIuO"); + + BOOST_TEST(toString(cachedProgram) == toString(cachedProgramNoRepetitions)); + + for (size_t size = 1; size <= 6; ++size) + { + BOOST_REQUIRE(m_programCache.contains(steps.substr(0, size))); + BOOST_REQUIRE(cacheNoRepetitions.contains(steps.substr(0, size))); + BOOST_TEST( + toString(*cacheNoRepetitions.find(steps.substr(0, size))) == + toString(*m_programCache.find(steps.substr(0, size))) + ); + } +} + +BOOST_FIXTURE_TEST_CASE(optimiseProgram_should_reuse_the_longest_prefix_and_move_it_to_the_next_round, ProgramCacheFixture) +{ + BOOST_TEST(m_programCache.currentRound() == 0); + + m_programCache.optimiseProgram("Iu"); + m_programCache.optimiseProgram("Ia"); + m_programCache.startRound(1); + + BOOST_TEST(m_programCache.currentRound() == 1); + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu", "Ia"})); + BOOST_TEST(m_programCache.entries().find("I")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("Iu")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("Ia")->second.roundNumber == 0); + + m_programCache.optimiseProgram("IuOI"); + + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu", "Ia", "IuO", "IuOI"})); + BOOST_TEST(m_programCache.entries().find("I")->second.roundNumber == 1); + BOOST_TEST(m_programCache.entries().find("Iu")->second.roundNumber == 1); + BOOST_TEST(m_programCache.entries().find("Ia")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("IuO")->second.roundNumber == 1); + BOOST_TEST(m_programCache.entries().find("IuOI")->second.roundNumber == 1); +} + +BOOST_FIXTURE_TEST_CASE(startRound_should_remove_entries_older_than_two_rounds, ProgramCacheFixture) +{ + BOOST_TEST(m_programCache.currentRound() == 0); + BOOST_TEST(m_programCache.size() == 0); + + m_programCache.optimiseProgram("Iu"); + + BOOST_TEST(m_programCache.currentRound() == 0); + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu"})); + BOOST_TEST(m_programCache.entries().find("I")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("Iu")->second.roundNumber == 0); + + m_programCache.optimiseProgram("a"); + + BOOST_TEST(m_programCache.currentRound() == 0); + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu", "a"})); + BOOST_TEST(m_programCache.entries().find("I")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("Iu")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("a")->second.roundNumber == 0); + + m_programCache.startRound(1); + + BOOST_TEST(m_programCache.currentRound() == 1); + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu", "a"})); + BOOST_TEST(m_programCache.entries().find("I")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("Iu")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("a")->second.roundNumber == 0); + + m_programCache.optimiseProgram("af"); + + BOOST_TEST(m_programCache.currentRound() == 1); + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"I", "Iu", "a", "af"})); + BOOST_TEST(m_programCache.entries().find("I")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("Iu")->second.roundNumber == 0); + BOOST_TEST(m_programCache.entries().find("a")->second.roundNumber == 1); + BOOST_TEST(m_programCache.entries().find("af")->second.roundNumber == 1); + + m_programCache.startRound(2); + + BOOST_TEST(m_programCache.currentRound() == 2); + BOOST_REQUIRE((cachedKeys(m_programCache) == set{"a", "af"})); + BOOST_TEST(m_programCache.entries().find("a")->second.roundNumber == 1); + BOOST_TEST(m_programCache.entries().find("af")->second.roundNumber == 1); + + m_programCache.startRound(3); + + BOOST_TEST(m_programCache.currentRound() == 3); + BOOST_TEST(m_programCache.size() == 0); +} + +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() + +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index fd5f92bbe..241a80f46 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(yul-phaser yulPhaser/PairSelections.cpp yulPhaser/Selections.h yulPhaser/Selections.cpp + yulPhaser/ProgramCache.h + yulPhaser/ProgramCache.cpp yulPhaser/Program.h yulPhaser/Program.cpp yulPhaser/SimulationRNG.h diff --git a/tools/yulPhaser/AlgorithmRunner.cpp b/tools/yulPhaser/AlgorithmRunner.cpp index b9f22171b..e7ea5a072 100644 --- a/tools/yulPhaser/AlgorithmRunner.cpp +++ b/tools/yulPhaser/AlgorithmRunner.cpp @@ -31,9 +31,12 @@ using namespace solidity::phaser; void AlgorithmRunner::run(GeneticAlgorithm& _algorithm) { populationAutosave(); + cacheClear(); for (size_t round = 0; !m_options.maxRounds.has_value() || round < m_options.maxRounds.value(); ++round) { + cacheStartRound(round + 1); + m_population = _algorithm.runNextRound(m_population); randomiseDuplicates(); @@ -66,6 +69,20 @@ void AlgorithmRunner::populationAutosave() const ); } +void AlgorithmRunner::cacheClear() +{ + for (auto& cache: m_programCaches) + if (cache != nullptr) + cache->clear(); +} + +void AlgorithmRunner::cacheStartRound(size_t _roundNumber) +{ + for (auto& cache: m_programCaches) + if (cache != nullptr) + cache->startRound(_roundNumber); +} + void AlgorithmRunner::randomiseDuplicates() { if (m_options.randomiseDuplicates) diff --git a/tools/yulPhaser/AlgorithmRunner.h b/tools/yulPhaser/AlgorithmRunner.h index ff0d0e3c3..8ee97ac48 100644 --- a/tools/yulPhaser/AlgorithmRunner.h +++ b/tools/yulPhaser/AlgorithmRunner.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -50,10 +51,12 @@ public: AlgorithmRunner( Population _initialPopulation, + std::vector> _programCaches, Options _options, std::ostream& _outputStream ): m_population(std::move(_initialPopulation)), + m_programCaches(std::move(_programCaches)), m_options(std::move(_options)), m_outputStream(_outputStream) {} @@ -65,6 +68,9 @@ public: private: void populationAutosave() const; void randomiseDuplicates(); + void cacheClear(); + void cacheStartRound(size_t _roundNumber); + static Population randomiseDuplicates( Population _population, size_t _minChromosomeLength, @@ -72,6 +78,7 @@ private: ); Population m_population; + std::vector> m_programCaches; Options m_options; std::ostream& m_outputStream; }; diff --git a/tools/yulPhaser/FitnessMetrics.cpp b/tools/yulPhaser/FitnessMetrics.cpp index 49482d0af..5279cea72 100644 --- a/tools/yulPhaser/FitnessMetrics.cpp +++ b/tools/yulPhaser/FitnessMetrics.cpp @@ -17,14 +17,36 @@ #include +#include + #include using namespace std; +using namespace solidity::util; using namespace solidity::phaser; -Program ProgramBasedMetric::optimisedProgram(Chromosome const& _chromosome) const +Program const& ProgramBasedMetric::program() const { - Program programCopy = m_program; + if (m_programCache == nullptr) + return m_program.value(); + else + return m_programCache->program(); +} + +Program ProgramBasedMetric::optimisedProgram(Chromosome const& _chromosome) +{ + if (m_programCache == nullptr) + return optimisedProgramNoCache(_chromosome); + + return m_programCache->optimiseProgram( + toString(_chromosome), + m_repetitionCount + ); +} + +Program ProgramBasedMetric::optimisedProgramNoCache(Chromosome const& _chromosome) const +{ + Program programCopy = program(); for (size_t i = 0; i < m_repetitionCount; ++i) programCopy.optimise(_chromosome.optimisationSteps()); diff --git a/tools/yulPhaser/FitnessMetrics.h b/tools/yulPhaser/FitnessMetrics.h index e79d99ff4..72e811152 100644 --- a/tools/yulPhaser/FitnessMetrics.h +++ b/tools/yulPhaser/FitnessMetrics.h @@ -22,8 +22,10 @@ #include #include +#include #include +#include namespace solidity::phaser { @@ -50,7 +52,7 @@ public: * Abstract base class for fitness metrics that return values based on program size. * * The class provides utilities for optimising programs according to the information stored in - * chromosomes. + * chromosomes. Allows using @a ProgramCache. * * It can also store weights for the @a CodeSize metric. It does not do anything with * them because it does not actually compute the code size but they are readily available for use @@ -60,19 +62,27 @@ class ProgramBasedMetric: public FitnessMetric { public: explicit ProgramBasedMetric( - Program _program, + std::optional _program, + std::shared_ptr _programCache, size_t _repetitionCount = 1 ): m_program(std::move(_program)), - m_repetitionCount(_repetitionCount) {} + m_programCache(std::move(_programCache)), + m_repetitionCount(_repetitionCount) + { + assert(m_program.has_value() == (m_programCache == nullptr)); + } - Program const& program() const { return m_program; } + Program const& program() const; + ProgramCache const* programCache() const { return m_programCache.get(); } size_t repetitionCount() const { return m_repetitionCount; } - Program optimisedProgram(Chromosome const& _chromosome) const; + Program optimisedProgram(Chromosome const& _chromosome); + Program optimisedProgramNoCache(Chromosome const& _chromosome) const; private: - Program m_program; + std::optional m_program; + std::shared_ptr m_programCache; size_t m_repetitionCount; }; @@ -98,11 +108,12 @@ class RelativeProgramSize: public ProgramBasedMetric { public: explicit RelativeProgramSize( - Program _program, + std::optional _program, + std::shared_ptr _programCache, size_t _fixedPointPrecision, size_t _repetitionCount = 1 ): - ProgramBasedMetric(std::move(_program), _repetitionCount), + ProgramBasedMetric(std::move(_program), std::move(_programCache), _repetitionCount), m_fixedPointPrecision(_fixedPointPrecision) {} size_t fixedPointPrecision() const { return m_fixedPointPrecision; } diff --git a/tools/yulPhaser/Phaser.cpp b/tools/yulPhaser/Phaser.cpp index b29938f22..218b358a0 100644 --- a/tools/yulPhaser/Phaser.cpp +++ b/tools/yulPhaser/Phaser.cpp @@ -158,9 +158,11 @@ FitnessMetricFactory::Options FitnessMetricFactory::Options::fromCommandLine(po: unique_ptr FitnessMetricFactory::build( Options const& _options, - vector _programs + vector _programs, + vector> _programCaches ) { + assert(_programCaches.size() == _programs.size()); assert(_programs.size() > 0 && "Validations should prevent this from being executed with zero files."); vector> metrics; @@ -168,9 +170,10 @@ unique_ptr FitnessMetricFactory::build( { case MetricChoice::CodeSize: { - for (Program& program: _programs) + for (size_t i = 0; i < _programs.size(); ++i) metrics.push_back(make_unique( - move(program), + _programCaches[i] != nullptr ? optional{} : move(_programs[i]), + move(_programCaches[i]), _options.chromosomeRepetitions )); @@ -178,9 +181,10 @@ unique_ptr FitnessMetricFactory::build( } case MetricChoice::RelativeCodeSize: { - for (Program& program: _programs) + for (size_t i = 0; i < _programs.size(); ++i) metrics.push_back(make_unique( - move(program), + _programCaches[i] != nullptr ? optional{} : move(_programs[i]), + move(_programCaches[i]), _options.relativeMetricScale, _options.chromosomeRepetitions )); @@ -281,6 +285,25 @@ Population PopulationFactory::buildFromFile( return buildFromStrings(readLinesFromFile(_filePath), move(_fitnessMetric)); } +ProgramCacheFactory::Options ProgramCacheFactory::Options::fromCommandLine(po::variables_map const& _arguments) +{ + return { + _arguments["program-cache"].as(), + }; +} + +vector> ProgramCacheFactory::build( + Options const& _options, + vector _programs +) +{ + vector> programCaches; + for (Program& program: _programs) + programCaches.push_back(_options.programCacheEnabled ? make_shared(move(program)) : nullptr); + + return programCaches; +} + ProgramFactory::Options ProgramFactory::Options::fromCommandLine(po::variables_map const& _arguments) { return { @@ -507,6 +530,19 @@ Phaser::CommandLineDescription Phaser::buildCommandLineDescription() ; keywordDescription.add(metricsDescription); + po::options_description cacheDescription("CACHE", lineLength, minDescriptionLength); + cacheDescription.add_options() + ( + "program-cache", + po::bool_switch(), + "Enables caching of intermediate programs corresponding to chromosome prefixes.\n" + "This speeds up fitness evaluation by a lot but eats tons of memory if the chromosomes are long. " + "Disabled by default since there's currently no way to set an upper limit on memory usage but " + "highly recommended if your computer has enough RAM." + ) + ; + keywordDescription.add(cacheDescription); + po::positional_options_description positionalDescription; positionalDescription.add("input-files", -1); @@ -562,12 +598,15 @@ AlgorithmRunner::Options Phaser::buildAlgorithmRunnerOptions(po::variables_map c void Phaser::runAlgorithm(po::variables_map const& _arguments) { auto programOptions = ProgramFactory::Options::fromCommandLine(_arguments); + auto cacheOptions = ProgramCacheFactory::Options::fromCommandLine(_arguments); auto metricOptions = FitnessMetricFactory::Options::fromCommandLine(_arguments); auto populationOptions = PopulationFactory::Options::fromCommandLine(_arguments); auto algorithmOptions = GeneticAlgorithmFactory::Options::fromCommandLine(_arguments); vector programs = ProgramFactory::build(programOptions); - unique_ptr fitnessMetric = FitnessMetricFactory::build(metricOptions, move(programs)); + vector> programCaches = ProgramCacheFactory::build(cacheOptions, programs); + + unique_ptr fitnessMetric = FitnessMetricFactory::build(metricOptions, move(programs), programCaches); Population population = PopulationFactory::build(populationOptions, move(fitnessMetric)); unique_ptr geneticAlgorithm = GeneticAlgorithmFactory::build( @@ -575,6 +614,6 @@ void Phaser::runAlgorithm(po::variables_map const& _arguments) population.individuals().size() ); - AlgorithmRunner algorithmRunner(population, buildAlgorithmRunnerOptions(_arguments), cout); + AlgorithmRunner algorithmRunner(population, move(programCaches), buildAlgorithmRunnerOptions(_arguments), cout); algorithmRunner.run(*geneticAlgorithm); } diff --git a/tools/yulPhaser/Phaser.h b/tools/yulPhaser/Phaser.h index 9814c9111..ba816044c 100644 --- a/tools/yulPhaser/Phaser.h +++ b/tools/yulPhaser/Phaser.h @@ -45,6 +45,7 @@ class FitnessMetric; class GeneticAlgorithm; class Population; class Program; +class ProgramCache; enum class Algorithm { @@ -119,7 +120,8 @@ public: static std::unique_ptr build( Options const& _options, - std::vector _programs + std::vector _programs, + std::vector> _programCaches ); }; @@ -160,6 +162,25 @@ public: ); }; +/** + * Builds and validates instances of @a ProgramCache. + */ +class ProgramCacheFactory +{ +public: + struct Options + { + bool programCacheEnabled; + + static Options fromCommandLine(boost::program_options::variables_map const& _arguments); + }; + + static std::vector> build( + Options const& _options, + std::vector _programs + ); +}; + /** * Builds and validates instances of @a Program. */ diff --git a/tools/yulPhaser/ProgramCache.cpp b/tools/yulPhaser/ProgramCache.cpp new file mode 100644 index 000000000..bd3b05114 --- /dev/null +++ b/tools/yulPhaser/ProgramCache.cpp @@ -0,0 +1,94 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +#include + +#include + +using namespace std; +using namespace solidity::yul; +using namespace solidity::phaser; + +Program ProgramCache::optimiseProgram( + string const& _abbreviatedOptimisationSteps, + size_t _repetitionCount +) +{ + string targetOptimisations = _abbreviatedOptimisationSteps; + for (size_t i = 1; i < _repetitionCount; ++i) + targetOptimisations += _abbreviatedOptimisationSteps; + + size_t prefixSize = 0; + for (size_t i = 1; i <= targetOptimisations.size(); ++i) + { + auto const& pair = m_entries.find(targetOptimisations.substr(0, i)); + if (pair != m_entries.end()) + { + pair->second.roundNumber = m_currentRound; + ++prefixSize; + } + else + break; + } + + Program intermediateProgram = ( + prefixSize == 0 ? + m_program : + m_entries.at(targetOptimisations.substr(0, prefixSize)).program + ); + + for (size_t i = prefixSize + 1; i <= targetOptimisations.size(); ++i) + { + string stepName = OptimiserSuite::stepAbbreviationToNameMap().at(targetOptimisations[i - 1]); + intermediateProgram.optimise({stepName}); + + m_entries.insert({targetOptimisations.substr(0, i), {intermediateProgram, m_currentRound}}); + } + + return intermediateProgram; +} + +void ProgramCache::startRound(size_t _roundNumber) +{ + assert(_roundNumber > m_currentRound); + m_currentRound = _roundNumber; + + for (auto pair = m_entries.begin(); pair != m_entries.end();) + { + assert(pair->second.roundNumber < m_currentRound); + + if (pair->second.roundNumber < m_currentRound - 1) + m_entries.erase(pair++); + else + ++pair; + } +} + +void ProgramCache::clear() +{ + m_entries.clear(); + m_currentRound = 0; +} + +Program const* ProgramCache::find(string const& _abbreviatedOptimisationSteps) const +{ + auto const& pair = m_entries.find(_abbreviatedOptimisationSteps); + if (pair == m_entries.end()) + return nullptr; + + return &(pair->second.program); +} diff --git a/tools/yulPhaser/ProgramCache.h b/tools/yulPhaser/ProgramCache.h new file mode 100644 index 000000000..433ba99c5 --- /dev/null +++ b/tools/yulPhaser/ProgramCache.h @@ -0,0 +1,91 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +#pragma once + +#include + +#include +#include + +namespace solidity::phaser +{ + +/** + * Structure used by @a ProgramCache to store intermediate programs and metadata associated + * with them. + */ +struct CacheEntry +{ + Program program; + size_t roundNumber; + + CacheEntry(Program _program, size_t _roundNumber): + program(std::move(_program)), + roundNumber(_roundNumber) {} +}; + +/** + * Class that optimises programs one step at a time which allows it to store and later reuse the + * results of the intermediate steps. + * + * The cache keeps track of the current round number and associates newly created entries with it. + * @a startRound() must be called at the beginning of a round so that entries that are too old + * can be purged. The current strategy is to store programs corresponding to all possible prefixes + * encountered in the current and the previous rounds. Entries older than that get removed to + * conserve memory. + * + * The current strategy does speed things up (about 4:1 hit:miss ratio observed in my limited + * experiments) but there's room for improvement. We could fit more useful programs in + * the cache by being more picky about which ones we choose. + * + * There is currently no way to purge entries without starting a new round. Since the programs + * take a lot of memory, this may lead to the cache eating up all the available RAM if sequences are + * long and programs large. A limiter based on entry count or total program size would be useful. + */ +class ProgramCache +{ +public: + explicit ProgramCache(Program _program): + m_program(std::move(_program)) {} + + Program optimiseProgram( + std::string const& _abbreviatedOptimisationSteps, + size_t _repetitionCount = 1 + ); + void startRound(size_t _nextRoundNumber); + void clear(); + + size_t size() const { return m_entries.size(); } + Program const* find(std::string const& _abbreviatedOptimisationSteps) const; + bool contains(std::string const& _abbreviatedOptimisationSteps) const { return find(_abbreviatedOptimisationSteps) != nullptr; } + + std::map const& entries() const { return m_entries; }; + Program const& program() const { return m_program; } + size_t currentRound() const { return m_currentRound; } + +private: + // The best matching data structure here would be a trie of chromosome prefixes but since + // the programs are orders of magnitude larger than the prefixes, it does not really matter. + // A map should be good enough. + std::map m_entries; + + Program m_program; + size_t m_currentRound = 0; +}; + +}