Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/internals/optimizer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ simplifies to this:
data[7] = 9;
return 1;

Constant Optimizer
------------------

The opcode-based constant optimizer can replace large literal constants by shorter code that
computes the same value. For byte-aligned, right-aligned masks of ``0xff`` bytes that would
otherwise be emitted literally or computed, the compiler may reserve a memory area containing a
zero word followed by an all-ones word and materialize the mask with ``MLOAD``. This behavior is
controlled by the Standard JSON ``memoryMasks`` optimizer detail and defaults to enabled when
either the constant optimizer or the Yul optimizer is enabled and the optimizer ``runs`` value is at
most ``200``.

Simple Inlining
---------------

Expand Down
4 changes: 4 additions & 0 deletions docs/using-the-compiler.rst
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ Input Description
// Tries to find better representations of literal numbers and strings, that satisfy the
// size/cost trade-off determined by the 'runs' setting.
"constantOptimizer": false,
// Memory mask constants. Optional. Default: true when either constant or Yul optimizer is enabled and 'runs' <= 200.
// Uses a reserved memory region to materialize selected mask constants that the constant
// optimizer would otherwise compute.
"memoryMasks": false,
// Unchecked loop increment (codegen-based). Optional. Default: true.
// Use unchecked arithmetic when incrementing the counter of 'for' loops under certain circumstances.
// NOTE: Always runs (even with optimization disabled) unless explicitly turned off here.
Expand Down
4 changes: 3 additions & 1 deletion libevmasm/Assembly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ std::map<u256, u256> const& Assembly::optimiseInternal(
isCreation(),
isCreation() ? 1 : _settings.expectedExecutionsPerDeployment,
m_evmVersion,
_settings.useMemoryMasks && !isCreation(),
*this
);

Expand Down Expand Up @@ -1862,13 +1863,14 @@ Assembly const* Assembly::subAssemblyById(SubAssemblyID const _subId) const
Assembly::OptimiserSettings Assembly::OptimiserSettings::translateSettings(frontend::OptimiserSettings const& _settings)
{
// Constructing it this way so that we notice changes in the fields.
OptimiserSettings asmSettings{false, false, false, false, false, false, 0};
OptimiserSettings asmSettings{false, false, false, false, false, false, false, 0};
asmSettings.runInliner = _settings.runInliner;
asmSettings.runJumpdestRemover = _settings.runJumpdestRemover;
asmSettings.runPeephole = _settings.runPeephole;
asmSettings.runDeduplicate = _settings.runDeduplicate;
asmSettings.runCSE = _settings.runCSE;
asmSettings.runConstantOptimiser = _settings.runConstantOptimiser;
asmSettings.useMemoryMasks = _settings.useMemoryMasks();
asmSettings.expectedExecutionsPerDeployment = _settings.expectedExecutionsPerDeployment;
return asmSettings;
}
1 change: 1 addition & 0 deletions libevmasm/Assembly.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class Assembly
bool runDeduplicate = false;
bool runCSE = false;
bool runConstantOptimiser = false;
bool useMemoryMasks = false;
/// This specifies an estimate on how often each opcode in this assembly will be executed,
/// i.e. use a small value to optimise for size and a large value to optimise for runtime gas usage.
size_t expectedExecutionsPerDeployment = frontend::OptimiserSettings{}.expectedExecutionsPerDeployment;
Expand Down
2 changes: 2 additions & 0 deletions libevmasm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ set(sources
KnownState.h
LinkerObject.cpp
LinkerObject.h
MemoryMasking.cpp
MemoryMasking.h
PathGasMeter.cpp
PathGasMeter.h
PeepholeOptimiser.cpp
Expand Down
50 changes: 40 additions & 10 deletions libevmasm/ConstantOptimiser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <libevmasm/ConstantOptimiser.h>
#include <libevmasm/Assembly.h>
#include <libevmasm/GasMeter.h>
#include <libevmasm/MemoryMasking.h>

using namespace solidity;
using namespace solidity::evmasm;
Expand All @@ -31,6 +32,7 @@ unsigned ConstantOptimisationMethod::optimiseConstants(
bool _isCreation,
size_t _runs,
langutil::EVMVersion _evmVersion,
bool _useMemoryMasks,
Assembly& _assembly
)
{
Expand All @@ -53,27 +55,38 @@ unsigned ConstantOptimisationMethod::optimiseConstants(
Params params;
params.multiplicity = it.second;
params.isCreation = _isCreation;
params.useMemoryMasks = _useMemoryMasks;
params.runs = _runs;
params.evmVersion = _evmVersion;
LiteralMethod lit(params, item.data());
bigint literalGas = lit.gasNeeded();
CodeCopyMethod copy(params, item.data());
bigint copyGas = copy.gasNeeded();
ComputeMethod compute(params, item.data());
bigint computeGas = compute.gasNeeded();
AssemblyItems replacement;
if (copyGas < literalGas && copyGas < computeGas)
if (params.useMemoryMasks)
{
replacement = copy.execute(_assembly);
optimisations++;
MemoryLoadMethod memoryLoad(params, item.data());
if (memoryLoad.valid() && memoryLoad.gasNeeded() < literalGas)
replacement = memoryLoad.execute(_assembly);
}
else if (computeGas < literalGas && computeGas <= copyGas)
if (replacement.empty())
{
replacement = compute.execute(_assembly);
optimisations++;
CodeCopyMethod copy(params, item.data());
bigint copyGas = copy.gasNeeded();
ComputeMethod compute(params, item.data());
bigint computeGas = compute.gasNeeded();
if (copyGas < literalGas && copyGas < computeGas)
{
replacement = copy.execute(_assembly);
}
else if (computeGas < literalGas && computeGas <= copyGas)
{
replacement = compute.execute(_assembly);
}
}
if (!replacement.empty())
{
optimisations++;
pendingReplacements[item.data()] = replacement;
}
}
if (!pendingReplacements.empty())
replaceConstants(_items, pendingReplacements);
Expand Down Expand Up @@ -389,3 +402,20 @@ bigint ComputeMethod::gasNeeded(AssemblyItems const& _routine) const
0
);
}

MemoryLoadMethod::MemoryLoadMethod(Params const& _params, u256 const& _value):
ConstantOptimisationMethod(_params, _value)
{
if (std::optional<size_t> offset = MemoryMasking::offsetForRightAlignedOnes(_value))
m_routine = AssemblyItems{u256(*offset), Instruction::MLOAD};
}

bigint MemoryLoadMethod::gasNeeded() const
{
solAssert(valid(), "");
return combineGas(
simpleRunGas(m_routine, m_params.evmVersion),
bytesRequired(m_routine, m_params.evmVersion) * (m_params.isCreation ? GasCosts::txDataNonZeroGas(m_params.evmVersion) : GasCosts::createDataGas),
0
);
}
18 changes: 18 additions & 0 deletions libevmasm/ConstantOptimiser.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ConstantOptimisationMethod
bool _isCreation,
size_t _runs,
langutil::EVMVersion _evmVersion,
bool _useMemoryMasks,
Assembly& _assembly
);

Expand All @@ -60,6 +61,7 @@ class ConstantOptimisationMethod
struct Params
{
bool isCreation; ///< Whether this is called during contract creation or runtime.
bool useMemoryMasks; ///< Whether the reserved memory mask region may be used.
size_t runs; ///< Estimated number of calls per opcode oven the lifetime of the contract.
size_t multiplicity; ///< Number of times the constant appears in the code.
langutil::EVMVersion evmVersion; ///< Version of the EVM
Expand Down Expand Up @@ -150,4 +152,20 @@ class ComputeMethod: public ConstantOptimisationMethod
AssemblyItems m_routine;
};

/**
* Method that loads byte-aligned right-aligned masks from the reserved memory mask region.
*/
class MemoryLoadMethod: public ConstantOptimisationMethod
{
public:
MemoryLoadMethod(Params const& _params, u256 const& _value);

bool valid() const { return !m_routine.empty(); }
bigint gasNeeded() const override;
AssemblyItems execute(Assembly&) const override { return m_routine; }

private:
AssemblyItems m_routine;
};

}
49 changes: 49 additions & 0 deletions libevmasm/MemoryMasking.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
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 <libevmasm/MemoryMasking.h>

using namespace solidity;
using namespace solidity::evmasm;

std::optional<size_t> MemoryMasking::offsetForRightAlignedOnes(u256 const& _value)
{
u256 mask = 0;
for (size_t bytes = 1; bytes < maskSize; ++bytes)
{
mask <<= 8;
mask |= 0xff;
if (bytes > 2 && _value == mask)
return zeroPointer + bytes;
}
return std::nullopt;
}

std::optional<u256> MemoryMasking::constantForOffset(size_t _offset)
{
if (_offset < zeroPointer || _offset > maskPointer)
return std::nullopt;

u256 value = 0;
for (size_t bytes = 0; bytes < _offset - zeroPointer; ++bytes)
{
value <<= 8;
value |= 0xff;
}
return value;
}
47 changes: 47 additions & 0 deletions libevmasm/MemoryMasking.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
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
/**
* Helpers for the memory region used to materialize common mask constants.
*/

#pragma once

#include <libsolutil/Numeric.h>

#include <cstddef>
#include <optional>

namespace solidity::evmasm
{

struct MemoryMasking
{
static size_t constexpr zeroPointer = 0x60;
static size_t constexpr maskPointer = 0x80;
static size_t constexpr maskSize = 32;
static size_t constexpr memoryStart = maskPointer + maskSize;

/// @returns the offset in the zero/ones memory region whose MLOAD result is @a _value,
/// if @a _value is a right-aligned byte mask wider than two bytes.
static std::optional<size_t> offsetForRightAlignedOnes(u256 const& _value);

/// @returns the constant produced by MLOADing at @a _offset in the zero/ones memory region.
static std::optional<u256> constantForOffset(size_t _offset);
};

}
1 change: 1 addition & 0 deletions libsolidity/codegen/Compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ void Compiler::compileContract(
// This might modify m_runtimeContext because it can access runtime functions at
// creation time.
OptimiserSettings creationSettings{m_optimiserSettings};
creationSettings.enableMemoryMasks = false;
// The creation code will be executed at most once, so we modify the optimizer
// settings accordingly.
creationSettings.expectedExecutionsPerDeployment = 1;
Expand Down
8 changes: 7 additions & 1 deletion libsolidity/codegen/CompilerContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,16 @@ void CompilerContext::addImmutable(VariableDeclaration const& _variable)
solAssert(_variable.immutable(), "Attempted to register a non-immutable variable as immutable.");
solUnimplementedAssert(_variable.annotation().type->isValueType(), "Only immutable variables of value type are supported.");
solAssert(m_runtimeContext, "Attempted to register an immutable variable for runtime code generation.");
m_immutableVariables[&_variable] = CompilerUtils::generalPurposeMemoryStart + *m_reservedMemory;
m_immutableVariables[&_variable] = generalPurposeMemoryStart() + *m_reservedMemory;
solAssert(_variable.annotation().type->memoryHeadSize() == 32, "Memory writes might overlap.");
*m_reservedMemory += _variable.annotation().type->memoryHeadSize();
}

size_t CompilerContext::generalPurposeMemoryStart() const
{
return CompilerUtils::generalPurposeMemoryStartFor(m_useMemoryMasks);
}

size_t CompilerContext::immutableMemoryOffset(VariableDeclaration const& _variable) const
{
solAssert(m_immutableVariables.count(&_variable), "Memory offset of unknown immutable queried.");
Expand Down Expand Up @@ -550,6 +555,7 @@ void CompilerContext::optimizeYul(yul::Object& _object, OptimiserSettings const&
_optimiserSettings.yulOptimiserSteps,
_optimiserSettings.yulOptimiserCleanupSteps,
isCreation? std::nullopt : std::make_optional(_optimiserSettings.expectedExecutionsPerDeployment),
_optimiserSettings.useMemoryMasks(),
_externalIdentifiers
);

Expand Down
5 changes: 5 additions & 0 deletions libsolidity/codegen/CompilerContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class CompilerContext

langutil::EVMVersion const& evmVersion() const { return m_evmVersion; }

void setUseMemoryMasks(bool _value) { m_useMemoryMasks = _value; }
bool useMemoryMasks() const { return m_useMemoryMasks; }
size_t generalPurposeMemoryStart() const;

void setUseABICoderV2(bool _value) { m_useABICoderV2 = _value; }
bool useABICoderV2() const { return m_useABICoderV2; }

Expand Down Expand Up @@ -352,6 +356,7 @@ class CompilerContext
/// Version of the EVM to compile against.
langutil::EVMVersion m_evmVersion;
RevertStrings const m_revertStrings;
bool m_useMemoryMasks = false;
bool m_useABICoderV2 = false;
/// Other already compiled contracts to be used in contract creation calls.
std::map<ContractDefinition const*, std::shared_ptr<Compiler const>> m_otherCompilers;
Expand Down
17 changes: 15 additions & 2 deletions libsolidity/codegen/CompilerUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <libsolidity/codegen/ArrayUtils.h>
#include <libsolidity/codegen/LValue.h>
#include <libsolutil/FunctionSelector.h>
#include <libevmasm/MemoryMasking.h>
#include <libevmasm/Instruction.h>
#include <libsolutil/Whiskers.h>
#include <libsolutil/StackTooDeepString.h>
Expand All @@ -45,17 +46,29 @@ using solidity::toCompactHexWithPrefix;
unsigned const CompilerUtils::dataStartOffset = 4;
size_t const CompilerUtils::freeMemoryPointer = 64;
size_t const CompilerUtils::zeroPointer = CompilerUtils::freeMemoryPointer + 32;
size_t const CompilerUtils::maskMemoryPointer = evmasm::MemoryMasking::maskPointer;
size_t const CompilerUtils::generalPurposeMemoryStart = CompilerUtils::zeroPointer + 32;
size_t const CompilerUtils::generalPurposeMemoryStartWithMemoryMasks = evmasm::MemoryMasking::memoryStart;

static_assert(CompilerUtils::freeMemoryPointer >= 64, "Free memory pointer must not overlap with scratch area.");
static_assert(CompilerUtils::zeroPointer >= CompilerUtils::freeMemoryPointer + 32, "Zero pointer must not overlap with free memory pointer.");
static_assert(CompilerUtils::generalPurposeMemoryStart >= CompilerUtils::zeroPointer + 32, "General purpose memory must not overlap with zero area.");
static_assert(CompilerUtils::maskMemoryPointer >= CompilerUtils::generalPurposeMemoryStart, "Mask pointer must not overlap with zero area.");
static_assert(CompilerUtils::generalPurposeMemoryStartWithMemoryMasks >= CompilerUtils::maskMemoryPointer + 32, "General purpose memory must not overlap with mask area.");

size_t CompilerUtils::generalPurposeMemoryStartFor(bool _useMemoryMasks)
{
return _useMemoryMasks ? generalPurposeMemoryStartWithMemoryMasks : generalPurposeMemoryStart;
}

void CompilerUtils::initialiseFreeMemoryPointer()
{
size_t reservedMemory = m_context.reservedMemory();
solAssert(bigint(generalPurposeMemoryStart) + bigint(reservedMemory) < bigint(1) << 63);
m_context << (u256(generalPurposeMemoryStart) + reservedMemory);
size_t const memoryStart = m_context.generalPurposeMemoryStart();
solAssert(bigint(memoryStart) + bigint(reservedMemory) < bigint(1) << 63);
if (m_context.useMemoryMasks())
m_context << u256(0) << Instruction::NOT << u256(maskMemoryPointer) << Instruction::MSTORE;
m_context << (u256(memoryStart) + reservedMemory);
storeFreeMemoryPointer();
}

Expand Down
5 changes: 5 additions & 0 deletions libsolidity/codegen/CompilerUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,13 @@ class CompilerUtils
static size_t const freeMemoryPointer;
/// Position of the memory slot that is always zero.
static size_t const zeroPointer;
/// Position of the memory slot containing all ones when memory masks are enabled.
static size_t const maskMemoryPointer;
/// Starting offset for memory available to the user (aka the contract).
static size_t const generalPurposeMemoryStart;
/// Starting offset for memory available to the user when memory masks are enabled.
static size_t const generalPurposeMemoryStartWithMemoryMasks;
static size_t generalPurposeMemoryStartFor(bool _useMemoryMasks);

private:
/// Appends code that cleans higher-order bits for integer types.
Expand Down
Loading