Skip to content

Commit

Permalink
Replace CompactValue with StyleValueHandle and StyleValuePool
Browse files Browse the repository at this point in the history
Summary:
X-link: facebook/react-native#42131

X-link: facebook/yoga#1534

Now that the storage method is a hidden implementation detail, this changes the underlying data structure used to store styles, from `CompactValue` (a customized 32-bit float with tag bits), to `StyleValuePool`.

This new structure operates on 16-bit handles, and a shared small buffer. The vast majority of real-world values can be stored directly in the handle, but we allow arbitrary 32 bit (and soon 64-bit) values to be stored, where the handle then becomes an index into the styles buffer.

This results in a real-world memory usage win, while also letting us store the 64-bit values we are wanting to use for math function support (compared to doubling the storage requirements).

This does seem to make style reads slower, which due to their heavy frequency, does have a performance impact observable by synthetics. In an example laying out a tree of 10,000 nodes, we originally read from `StyleValuePool` 2.4 million times.

This originally resulted in a ~10% regression, but when combined with the changes in the last diff, most style reads become simple bitwise operations on the handle, and we are actually 14% faster than before.

| | Before | After | Δ |
| `sizeof(yoga::Style)` | 208B  | 144B | -64B/-31% |
| `sizeof(yoga::Node)` | 640B  | 576B | -64B/-10% |
| `sizeof(YogaLayoutableShadowNode) ` |  920B | 856B | -64B/-7% |
| `sizeof(YogaLayoutableShadowNode) + sizeof(YogaStylableProps)` | 1296B  | 1168B | -128B/-10% |
| `sizeof(ViewShadowNode)`  |  920B | 856B | -64B/-7% |
| `sizeof(ViewShadowNode) + sizeof(ViewShadowNodeProps)` | 2000B  | 1872B | -128B/-6% |
| "Huge nested layout" microbenchmark (M1 Ultra) | 11.5ms | 9.9ms | -1.6ms/-14% |
| Quest Store C++ heap usage (avg over 10 runs) | 86.2MB | 84.9MB | -1.3MB/-1.5% |

Reviewed By: joevilches

Differential Revision: D52223122

fbshipit-source-id: 990f4b7e991e8e22d198ce20f7da66d9c6ba637b
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Jan 20, 2024
1 parent 080ef9c commit 223a9f6
Show file tree
Hide file tree
Showing 7 changed files with 478 additions and 260 deletions.
16 changes: 7 additions & 9 deletions lib/yoga/src/main/cpp/yoga/YGNodeStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ void updateStyle(YGNodeRef node, IdxT idx, ValueT value) {

} // namespace

void YGNodeCopyStyle(
const YGNodeRef dstNodeRef,
const YGNodeConstRef srcNodeRef) {
auto dstNode = resolveRef(dstNodeRef);
auto srcNode = resolveRef(srcNodeRef);

if (!(dstNode->style() == srcNode->style())) {
dstNode->setStyle(srcNode->style());
dstNode->markDirtyAndPropagate();
void YGNodeCopyStyle(YGNodeRef dstNode, YGNodeConstRef srcNode) {
auto dst = resolveRef(dstNode);
auto src = resolveRef(srcNode);

if (dst->style() != src->style()) {
dst->setStyle(src->style());
dst->markDirtyAndPropagate();
}
}

Expand Down
8 changes: 4 additions & 4 deletions lib/yoga/src/main/cpp/yoga/node/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,13 @@ class YG_EXPORT Node : public ::YGNode {
YGMeasureFunc measureFunc_ = nullptr;
YGBaselineFunc baselineFunc_ = nullptr;
YGDirtiedFunc dirtiedFunc_ = nullptr;
Style style_ = {};
LayoutResults layout_ = {};
Style style_;
LayoutResults layout_;
size_t lineIndex_ = 0;
Node* owner_ = nullptr;
std::vector<Node*> children_ = {};
std::vector<Node*> children_;
const Config* config_;
std::array<Style::Length, 2> resolvedDimensions_ = {
std::array<Style::Length, 2> resolvedDimensions_{
{value::undefined(), value::undefined()}};
};

Expand Down
177 changes: 0 additions & 177 deletions lib/yoga/src/main/cpp/yoga/style/CompactValue.h

This file was deleted.

133 changes: 133 additions & 0 deletions lib/yoga/src/main/cpp/yoga/style/SmallValueBuffer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <array>
#include <bitset>
#include <cassert>
#include <cstdint>
#include <memory>
#include <vector>

namespace facebook::yoga {

// Container which allows storing 32 or 64 bit integer values, whose index may
// never change. Values are first stored in a fixed buffer of `BufferSize`
// 32-bit chunks, before falling back to heap allocation.
template <size_t BufferSize>
class SmallValueBuffer {
public:
SmallValueBuffer() = default;
SmallValueBuffer(const SmallValueBuffer& other) {
*this = other;
}
SmallValueBuffer(SmallValueBuffer&& other) = default;

// Add a new element to the buffer, returning the index of the element
uint16_t push(uint32_t value) {
const auto index = count_++;
assert(index < 4096 && "SmallValueBuffer can only hold up to 4096 chunks");
if (index < buffer_.size()) {
buffer_[index] = value;
return index;
}

if (overflow_ == nullptr) {
overflow_ = std::make_unique<SmallValueBuffer::Overflow>();
}

overflow_->buffer_.push_back(value);
overflow_->wideElements_.push_back(false);
return index;
}

uint16_t push(uint64_t value) {
const auto lsb = static_cast<uint32_t>(value & 0xFFFFFFFF);
const auto msb = static_cast<uint32_t>(value >> 32);

const auto lsbIndex = push(lsb);
[[maybe_unused]] const auto msbIndex = push(msb);
assert(
msbIndex < 4096 && "SmallValueBuffer can only hold up to 4096 chunks");

if (lsbIndex < buffer_.size()) {
wideElements_[lsbIndex] = true;
} else {
overflow_->wideElements_[lsbIndex - buffer_.size()] = true;
}
return lsbIndex;
}

// Replace an existing element in the buffer with a new value. A new index
// may be returned, e.g. if a new value is wider than the previous.
[[nodiscard]] uint16_t replace(uint16_t index, uint32_t value) {
if (index < buffer_.size()) {
buffer_[index] = value;
} else {
overflow_->buffer_.at(index - buffer_.size()) = value;
}

return index;
}

[[nodiscard]] uint16_t replace(uint16_t index, uint64_t value) {
const bool isWide = index < wideElements_.size()
? wideElements_[index]
: overflow_->wideElements_.at(index - buffer_.size());

if (isWide) {
const auto lsb = static_cast<uint32_t>(value & 0xFFFFFFFF);
const auto msb = static_cast<uint32_t>(value >> 32);

[[maybe_unused]] auto lsbIndex = replace(index, lsb);
[[maybe_unused]] auto msbIndex = replace(index + 1, msb);
return index;
} else {
return push(value);
}
}

// Get a value of a given width
uint32_t get32(uint16_t index) const {
if (index < buffer_.size()) {
return buffer_[index];
} else {
return overflow_->buffer_.at(index - buffer_.size());
}
}

uint64_t get64(uint16_t index) const {
const auto lsb = get32(index);
const auto msb = get32(index + 1);
return (static_cast<uint64_t>(msb) << 32) | lsb;
}

SmallValueBuffer& operator=(const SmallValueBuffer& other) {
count_ = other.count_;
buffer_ = other.buffer_;
wideElements_ = other.wideElements_;
overflow_ = other.overflow_ ? std::make_unique<Overflow>(*other.overflow_)
: nullptr;
return *this;
}

SmallValueBuffer& operator=(SmallValueBuffer&& other) = default;

private:
struct Overflow {
std::vector<uint32_t> buffer_;
std::vector<bool> wideElements_;
};

uint16_t count_{0};
std::array<uint32_t, BufferSize> buffer_;
std::bitset<BufferSize> wideElements_;
std::unique_ptr<Overflow> overflow_;
};

} // namespace facebook::yoga
Loading

0 comments on commit 223a9f6

Please sign in to comment.