Skip to content

Commit

Permalink
Caching of optimized IR
Browse files Browse the repository at this point in the history
  • Loading branch information
cameel committed Jul 31, 2024
1 parent 63671ad commit bf8c3d3
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 83 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Compiler Features:
* SMTChecker: Add CHC engine check for underflow and overflow in unary minus operation.
* SMTChecker: Replace CVC4 as a possible BMC backend with cvc5.
* Standard JSON Interface: Do not perform IR optimization when only unoptimized IR is requested.
* Yul Optimizer: Caching of optimized IR to speed up optimization of contracts with bytecode dependencies.
* Yul Optimizer: The optimizer now treats some previously unrecognized identical literals as identical.
* Commandline Interface: Allow the use of ``--asm-json`` output option in assembler mode to export EVM assembly of the contracts in JSON format.

Expand Down
7 changes: 5 additions & 2 deletions libsolidity/interface/CompilerStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ static int g_compilerStackCounts = 0;

CompilerStack::CompilerStack(ReadCallback::Callback _readFile):
m_readFile{std::move(_readFile)},
m_objectOptimizer(std::make_shared<yul::ObjectOptimizer>()),
m_errorReporter{m_errorList}
{
// Because TypeProvider is currently a singleton API, we must ensure that
Expand Down Expand Up @@ -1478,7 +1479,8 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti
m_eofVersion,
YulStack::Language::StrictAssembly,
m_optimiserSettings,
m_debugInfoSelection
m_debugInfoSelection,
m_objectOptimizer
);
bool yulAnalysisSuccessful = stack.parseAndAnalyze("", compiledContract.yulIR);
solAssert(
Expand Down Expand Up @@ -1515,7 +1517,8 @@ void CompilerStack::generateEVMFromIR(ContractDefinition const& _contract)
m_eofVersion,
yul::YulStack::Language::StrictAssembly,
m_optimiserSettings,
m_debugInfoSelection
m_debugInfoSelection,
m_objectOptimizer
);
bool analysisSuccessful = stack.parseAndAnalyze("", compiledContract.yulIROptimized);
solAssert(analysisSuccessful);
Expand Down
3 changes: 3 additions & 0 deletions libsolidity/interface/CompilerStack.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
#include <libsolutil/LazyInit.h>
#include <libsolutil/JSON.h>

#include <libyul/ObjectOptimizer.h>

#include <functional>
#include <memory>
#include <ostream>
Expand Down Expand Up @@ -543,6 +545,7 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac
std::shared_ptr<GlobalContext> m_globalContext;
std::vector<Source const*> m_sourceOrder;
std::map<std::string const, Contract> m_contracts;
std::shared_ptr<yul::ObjectOptimizer> m_objectOptimizer;

langutil::ErrorList m_errorList;
langutil::ErrorReporter m_errorReporter;
Expand Down
2 changes: 2 additions & 0 deletions libyul/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ add_library(yul
FunctionReferenceResolver.h
Object.cpp
Object.h
ObjectOptimizer.cpp
ObjectOptimizer.h
ObjectParser.cpp
ObjectParser.h
Scope.cpp
Expand Down
169 changes: 169 additions & 0 deletions libyul/ObjectOptimizer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0

#include <libyul/ObjectOptimizer.h>

#include <libyul/AsmAnalysisInfo.h>
#include <libyul/AsmAnalysis.h>
#include <libyul/AsmPrinter.h>
#include <libyul/AST.h>
#include <libyul/Exceptions.h>
#include <libyul/backends/evm/EVMDialect.h>
#include <libyul/backends/evm/EVMMetrics.h>
#include <libyul/optimiser/ASTCopier.h>
#include <libyul/optimiser/Suite.h>

#include <liblangutil/DebugInfoSelection.h>

#include <libsolutil/Keccak256.h>

#include <boost/algorithm/string.hpp>

#include <limits>
#include <numeric>

using namespace solidity;
using namespace solidity::langutil;
using namespace solidity::util;
using namespace solidity::yul;


Dialect const& yul::languageToDialect(Language _language, EVMVersion _version)
{
switch (_language)
{
case Language::Assembly:
case Language::StrictAssembly:
return EVMDialect::strictAssemblyForEVMObjects(_version);
case Language::Yul:
return EVMDialectTyped::instance(_version);
}
util::unreachable();
}

void ObjectOptimizer::optimize(Object& _object, Settings const& _settings)
{
yulAssert(_object.subId == std::numeric_limits<size_t>::max(), "Not a top-level object.");

optimize(_object, _settings, true /* _isCreation */);
}

void ObjectOptimizer::optimize(Object& _object, Settings const& _settings, bool _isCreation)
{
yulAssert(_object.code);
yulAssert(_object.debugData);

for (auto& subNode: _object.subObjects)
if (auto subObject = dynamic_cast<Object*>(subNode.get()))
{
bool isCreation = !boost::ends_with(subObject->name, "_deployed");
optimize(
*subObject,
_settings,
isCreation
);
}

Dialect const& dialect = languageToDialect(_settings.language, _settings.evmVersion);
std::unique_ptr<GasMeter> meter;
if (EVMDialect const* evmDialect = dynamic_cast<EVMDialect const*>(&dialect))
meter = std::make_unique<GasMeter>(*evmDialect, _isCreation, _settings.expectedExecutionsPerDeployment);

std::optional<h256> cacheKey = calculateCacheKey(*_object.code, *_object.debugData, _settings, _isCreation);
if (cacheKey.has_value() && m_cachedObjects.count(*cacheKey) != 0)
{
overwriteWithOptimizedObject(*cacheKey, _object);
return;
}

OptimiserSuite::run(
dialect,
meter.get(),
_object,
_settings.optimizeStackAllocation,
_settings.yulOptimiserSteps,
_settings.yulOptimiserCleanupSteps,
_isCreation ? std::nullopt : std::make_optional(_settings.expectedExecutionsPerDeployment),
{}
);

if (cacheKey.has_value())
storeOptimizedObject(*cacheKey, _object, dialect);
}

void ObjectOptimizer::storeOptimizedObject(util::h256 _cacheKey, Object const& _optimizedObject, Dialect const& _dialect)
{
m_cachedObjects[_cacheKey] = CachedObject{
std::make_shared<Block>(ASTCopier{}.translate(*_optimizedObject.code)),
&_dialect,
};
}

void ObjectOptimizer::overwriteWithOptimizedObject(util::h256 _cacheKey, Object& _object) const
{
yulAssert(m_cachedObjects.count(_cacheKey) != 0);
CachedObject const& cachedObject = m_cachedObjects.at(_cacheKey);

yulAssert(cachedObject.optimizedAST);
_object.code = std::make_shared<Block>(ASTCopier{}.translate(*cachedObject.optimizedAST));
yulAssert(_object.code);

// There's no point in caching AnalysisInfo because it references AST nodes. It can't be shared
// by multiple ASTs and it's easier to recalculate it than properly clone it.
yulAssert(cachedObject.dialect);
_object.analysisInfo = std::make_shared<AsmAnalysisInfo>(
AsmAnalyzer::analyzeStrictAssertCorrect(
*cachedObject.dialect,
_object
)
);

// NOTE: Source name index is included in the key so it must be identical. No need to store and restore it.
}

std::optional<h256> ObjectOptimizer::calculateCacheKey(
Block const& _ast,
ObjectDebugData const& _debugData,
Settings const& _settings,
bool _isCreation
)
{
AsmPrinter asmPrinter(
AsmPrinter::TypePrinting::OmitDefault,
languageToDialect(_settings.language, _settings.evmVersion),
_debugData.sourceNames,
DebugInfoSelection::All()
);

bytes rawKey;
// NOTE: AsmPrinter never prints nativeLocations included in debug data, so ASTs differing only
// in that regard are considered equal here. This is fine because the optimizer does not keep
// them up to date across AST transformations anyway so in any use where they need to be reliable,
// we just regenerate them by reparsing the object.
rawKey += keccak256(asmPrinter(_ast)).asBytes();
rawKey += keccak256(_debugData.formatUseSrcComment()).asBytes();
rawKey += h256(u256(_settings.language)).asBytes();
rawKey += FixedHash<1>(uint8_t(_settings.optimizeStackAllocation ? 0 : 1)).asBytes();
rawKey += h256(u256(_settings.expectedExecutionsPerDeployment)).asBytes();
rawKey += FixedHash<1>(uint8_t(_isCreation ? 0 : 1)).asBytes();
rawKey += keccak256(_settings.evmVersion.name()).asBytes();
rawKey += keccak256(_settings.yulOptimiserSteps).asBytes();
rawKey += keccak256(_settings.yulOptimiserCleanupSteps).asBytes();

return h256(keccak256(rawKey));
}
96 changes: 96 additions & 0 deletions libyul/ObjectOptimizer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0

#pragma once

#include <libyul/ASTForward.h>
#include <libyul/Object.h>

#include <liblangutil/EVMVersion.h>

#include <libsolutil/FixedHash.h>

#include <map>
#include <memory>
#include <optional>

namespace solidity::yul
{

enum class Language
{
Yul,
Assembly,
StrictAssembly,
};

Dialect const& languageToDialect(Language _language, langutil::EVMVersion _version);

/// Encapsulates logic for applying @a yul::OptimiserSuite to a whole hierarchy of Yul objects.
/// Also, acts as a transparent cache for optimized objects.
///
/// The cache is designed to allow sharing its instances widely across the compiler, without the
/// need to invalidate entries due to changing settings or context.
/// Caching is performed at the granularity of individual ASTs rather than whole object trees,
/// which means that reuse is possible even within a single hierarchy, e.g. when creation and
/// deployed objects have common dependencies.
class ObjectOptimizer
{
public:
/// Optimization settings and context information.
/// This information becomes a part of the cache key and, together with the object content,
/// must uniquely determine the result of optimization.
struct Settings
{
Language language;
langutil::EVMVersion evmVersion;
bool optimizeStackAllocation;
std::string yulOptimiserSteps;
std::string yulOptimiserCleanupSteps;
size_t expectedExecutionsPerDeployment;
};

/// Recursively optimizes a Yul object with given settings, reusing cached ASTs where possible
/// or caching the result otherwise. The object is modified in-place.
/// Automatically accounts for the difference between creation and deployed objects.
/// @warning Does not ensure that nativeLocations in the resulting AST match the optimized code.
void optimize(Object& _object, Settings const& _settings);

private:
struct CachedObject
{
std::shared_ptr<Block const> optimizedAST;
Dialect const* dialect;
};

void optimize(Object& _object, Settings const& _settings, bool _isCreation);

void storeOptimizedObject(util::h256 _cacheKey, Object const& _optimizedObject, Dialect const& _dialect);
void overwriteWithOptimizedObject(util::h256 _cacheKey, Object& _object) const;

static std::optional<util::h256> calculateCacheKey(
Block const& _ast,
ObjectDebugData const& _debugData,
Settings const& _settings,
bool _isCreation
);

std::map<util::h256, CachedObject> m_cachedObjects;
};

}
Loading

0 comments on commit bf8c3d3

Please sign in to comment.