Introduce til::generational - a struct comparison helper (#15088)

It can be costly, difficult, or often impossible to compare two
instances of a struct. This little helper can simplify this.

The underlying idea is that changes in state occur much less often than
the amount of data that's being processed in between. As such, this
helper assumes that _any_ modification to the struct it wraps is a
state change. When you compare the modified instance with another
the comparison operator will then always return false. This makes
state changes potentially more costly, because more state might be
invalidated than was necessary, but on the other hand it makes both,
the code simpler and the fast-path (no state change) much faster.

For instance, let's look at the amount of data that represents a
user's chosen font: It encompasses the font family, size and weight,
font axes (a vector of tuples), dpi and cell height/width overrides.
Comparing all that data, every time the user changes anything, is
fairly complex to code and maintain and costly at runtime, even though
the user will change the only font very seldomly. Instead, we can
optimize for the common case of no font changes occuring and simply
assume that if any font related field changed, all fields changed.
This is exactly what `til::generational` does.
This commit is contained in:
Leonard Hecker 2023-04-04 15:47:36 +02:00 committed by GitHub
parent 17cf44fa71
commit 5de1fd9a7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 3 deletions

View File

@ -10,4 +10,4 @@
\\tests(?![a-z])
\\thread(?![a-z])
\\tools(?![a-z])
\\types(?![a-z])
\\types?(?![a-z])

View File

@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
struct generation_t
{
auto operator<=>(const generation_t&) const = default;
constexpr void bump() noexcept
{
_value++;
}
uint32_t _value = 0;
};
// It can be costly, difficult, or often impossible to compare two
// instances of a struct. This little helper can simplify this.
//
// The underlying idea is that changes in state occur much less often than
// the amount of data that's being processed in between. As such, this
// helper assumes that _any_ modification to the struct it wraps is a
// state change. When you compare the modified instance with another
// the comparison operator will then always return false. This makes
// state changes potentially more costly, because more state might be
// invalidated than was necessary, but on the other hand it makes both,
// the code simpler and the fast-path (no state change) much faster.
template<typename T>
struct generational
{
generational() = default;
explicit constexpr generational(auto&&... args) :
_value{ std::forward<decltype(args)>(args)... } {}
explicit constexpr generational(generation_t generation, auto&&... args) :
_generation{ generation },
_value{ std::forward<decltype(args)>(args)... } {}
constexpr bool operator==(const generational& rhs) const noexcept { return generation() == rhs.generation(); }
constexpr bool operator!=(const generational& rhs) const noexcept { return generation() != rhs.generation(); }
constexpr generation_t generation() const noexcept
{
return _generation;
}
[[nodiscard]] constexpr const T* operator->() const noexcept
{
return &_value;
}
[[nodiscard]] constexpr const T& operator*() const noexcept
{
return _value;
}
[[nodiscard]] constexpr T* write() noexcept
{
_generation.bump();
return &_value;
}
private:
generation_t _generation;
T _value{};
};
}

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <til/generational.h>
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
struct Data
{
int value = 0;
};
class GenerationalTests
{
TEST_CLASS(GenerationalTests);
TEST_METHOD(Basic)
{
til::generational<Data> src;
til::generational<Data> dst;
// Reads via -> and *, just like std::optional, etc.
VERIFY_ARE_EQUAL(0, src->value);
VERIFY_ARE_EQUAL(0, (*src).value);
// Writes via .write()->
src.write()->value = 123;
// ...which makes them not compare as equal.
VERIFY_ARE_NOT_EQUAL(dst, src);
// Synchronize the two objects by copying them
dst = src;
// ...which results in both being considered equal again
VERIFY_ARE_EQUAL(dst, src);
// ...and all values are copied over.
VERIFY_ARE_EQUAL(123, dst->value);
}
};

View File

@ -20,6 +20,7 @@
<ClCompile Include="ColorTests.cpp" />
<ClCompile Include="EnumSetTests.cpp" />
<ClCompile Include="EnvTests.cpp" />
<ClCompile Include="GenerationalTests.cpp" />
<ClCompile Include="HashTests.cpp" />
<ClCompile Include="MathTests.cpp" />
<ClCompile Include="mutex.cpp" />
@ -43,10 +44,12 @@
<ClInclude Include="..\..\inc\til\atomic.h" />
<ClInclude Include="..\..\inc\til\bit.h" />
<ClInclude Include="..\..\inc\til\bitmap.h" />
<ClInclude Include="..\..\inc\til\bytes.h" />
<ClInclude Include="..\..\inc\til\coalesce.h" />
<ClInclude Include="..\..\inc\til\color.h" />
<ClInclude Include="..\..\inc\til\enumset.h" />
<ClInclude Include="..\..\inc\til\env.h" />
<ClInclude Include="..\..\inc\til\generational.h" />
<ClInclude Include="..\..\inc\til\hash.h" />
<ClInclude Include="..\..\inc\til\latch.h" />
<ClInclude Include="..\..\inc\til\math.h" />
@ -66,6 +69,7 @@
<ClInclude Include="..\..\inc\til\string.h" />
<ClInclude Include="..\..\inc\til\throttled_func.h" />
<ClInclude Include="..\..\inc\til\ticket_lock.h" />
<ClInclude Include="..\..\inc\til\type_traits.h" />
<ClInclude Include="..\..\inc\til\u8u16convert.h" />
<ClInclude Include="..\..\inc\til\unicode.h" />
<ClInclude Include="..\precomp.h" />
@ -85,4 +89,4 @@
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.build.tests.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
</Project>
</Project>

View File

@ -28,6 +28,7 @@
<ClCompile Include="u8u16convertTests.cpp" />
<ClCompile Include="EnvTests.cpp" />
<ClCompile Include="UnicodeTests.cpp" />
<ClCompile Include="GenerationalTests.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h" />
@ -118,10 +119,19 @@
<ClInclude Include="..\..\inc\til\unicode.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\..\inc\til\bytes.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\..\inc\til\generational.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\..\inc\til\type_traits.h">
<Filter>inc</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="inc">
<UniqueIdentifier>{7cf29ba4-d33d-4c3b-82e3-ab73e5a79685}</UniqueIdentifier>
</Filter>
</ItemGroup>
</Project>
</Project>

View File

@ -105,4 +105,11 @@
<Item Name="[ptr]">_ptr</Item>
</Expand>
</Type>
<Type Name="til::generational&lt;*&gt;">
<DisplayString>{{ gen={_generation._value}, {_value} }}</DisplayString>
<Expand>
<ExpandedItem>_value</ExpandedItem>
</Expand>
</Type>
</AutoVisualizer>