Commit bf27b4af authored by Phil Willoughby's avatar Phil Willoughby Committed by Facebook Github Bot

allocated size of Function

Summary:
The intent is to allow heavy users of Function to determine the runtime weight of each of them and evaluate functionally-equivalent options for their code structure (for example: lambda vs function vs `std::bind`) to pick the most space-efficient on their platform.

Limitations
=
If the real function type defines a custom `operator new` that allocates a different amount of memory than `sizeof(...)` suggests it should: that will not be accounted correctly.

We can't look into the real function type at what it itself has allocated. This means that if you capture a unique_ptr to `null` or a unique_ptr to a 4Gb buffer the reported size will be the same.

Reviewed By: ericniebler

Differential Revision: D14512815

fbshipit-source-id: dc636c8bebddabceaeaa8b869bf37c7f0c64c017
parent 3d77e1b3
......@@ -494,7 +494,7 @@ struct FunctionTraits<ReturnType(Args...) const noexcept> {
#endif
template <typename Fun>
bool execSmall(Op o, Data* src, Data* dst) {
std::size_t execSmall(Op o, Data* src, Data* dst) {
switch (o) {
case Op::MOVE:
::new (static_cast<void*>(&dst->tiny))
......@@ -506,11 +506,11 @@ bool execSmall(Op o, Data* src, Data* dst) {
case Op::HEAP:
break;
}
return false;
return 0U;
}
template <typename Fun>
bool execBig(Op o, Data* src, Data* dst) {
std::size_t execBig(Op o, Data* src, Data* dst) {
switch (o) {
case Op::MOVE:
dst->big = src->big;
......@@ -522,7 +522,7 @@ bool execBig(Op o, Data* src, Data* dst) {
case Op::HEAP:
break;
}
return true;
return sizeof(Fun);
}
} // namespace function
......@@ -541,7 +541,7 @@ class Function final : private detail::function::FunctionTraits<FunctionType> {
using Traits = detail::function::FunctionTraits<FunctionType>;
using Call = typename Traits::Call;
using Exec = bool (*)(Op, Data*, Data*);
using Exec = std::size_t (*)(Op, Data*, Data*);
template <typename Fun>
using IsSmall = detail::function::IsSmall<Fun>;
......@@ -554,8 +554,11 @@ class Function final : private detail::function::FunctionTraits<FunctionType> {
Call call_{&Traits::uninitCall};
Exec exec_{nullptr};
bool exec(Op o, Data* src, Data* dst) const {
return exec_ && exec_(o, src, dst);
std::size_t exec(Op o, Data* src, Data* dst) const {
if (!exec_) {
return 0U;
}
return exec_(o, src, dst);
}
friend Traits;
......@@ -799,12 +802,11 @@ class Function final : private detail::function::FunctionTraits<FunctionType> {
}
/**
* Returns `true` if this `Function` stores the callable on the
* heap. If `false` is returned, there has been no additional memory
* allocation and the callable is stored inside the `Function`
* object itself.
* Returns the size of the allocation made to store the callable on the
* heap. If `0` is returned, there has been no additional memory
* allocation because the callable is stored within the `Function` object.
*/
bool hasAllocatedMemory() const noexcept {
std::size_t heapAllocatedMemory() const noexcept {
return exec(Op::HEAP, nullptr, nullptr);
}
......
......@@ -82,7 +82,7 @@ class CompressionCounter {
return makeCompressionCounterHandler(
codecType, codecName, level, key, counterType);
};
assert(!initialize_.hasAllocatedMemory());
assert(!initialize_.heapAllocatedMemory());
}
void operator+=(double sum) {
......
......@@ -23,7 +23,7 @@ A workaround was available: `folly::MoveWrapper`, which wraps an object that may
`folly::Function` is an actual solution to this problem, as it can wrap non-copyable callables, at the cost of not being copy-constructible, which more often than not is not a relevant restriction. `folly::Future` now uses `folly::Function` to store callbacks, so the good news is: the code example from the top of this note is becoming a perfectly valid way to use future callbacks. The code compiles and behaves as you would expect.
Here are more details about `folly::Function`: much like `std::function`, it wraps an arbitrary object that can be invoked like a given function type. E.g. a `folly::Function<int(std::string, double)>` can wrap any callable object that returns an `int` (or something that is convertible to an `int`) when invoked with a `std::string` and a `double` argument. The function type is a template parameter of `folly::Function`, but the specific type of the callable is not. Also, like most implementations of `std::function`, `folly::Function` will store small callable objects in-place whereas larger callables will be stored on the heap. (Unlike `std::function`, you can set the size of the in-place storage as a template parameter of `folly::Function`.)
Here are more details about `folly::Function`: much like `std::function`, it wraps an arbitrary object that can be invoked like a given function type. E.g. a `folly::Function<int(std::string, double)>` can wrap any callable object that returns an `int` (or something that is convertible to an `int`) when invoked with a `std::string` and a `double` argument. The function type is a template parameter of `folly::Function`, but the specific type of the callable is not.
Other than copyability, there is one more significant difference between `std::function` and `folly::Function`, and it concerns const-correctness. `std::function` does not enforce const-correctness: it allows you to store mutable callables (i.e. callables that may change their inner state when executed, such as a mutable lambda) and call them in a const context (i.e. when you only have access to a const reference to the `std::function` object). For example:
``` Cpp
......@@ -85,4 +85,8 @@ If you want to go the other way, you are talking about a (potentially dangerous)
folly::Function<int() const> uf1c = folly::constCastFunction(std::move(uf1));
// uf1c() returns 1
```
Admittedly, seeing const function types as template parameters is unfamiliar. As far as I am aware it happens nowhere in the standard library. But it is the most consistent way to deal with the issue of const-correctness here. Const qualifiers are part of a function type, as a matter of fact. If you require a const-qualified function to be wrapped in a `folly::Function`, just declare it as that! More often than not you will find that you do not need the const qualifier. While writing the `folly::Function` implementation, a good set of unit tests had existed before the const function types got introduced. Not a single of those unit tests had to be changed: they all compiled and passed after the introduction of const function types. Obviously new ones were added to test the const-correctness. But in your day-to-day use of `folly::Function` you won’t have to worry about const very often.
Admittedly, seeing const function types as template parameters is unfamiliar. As far as I am aware it happens nowhere in the standard library. But it is the most consistent way to deal with the issue of const-correctness here. Const qualifiers are part of a function type, as a matter of fact. If you require a const-qualified function to be wrapped in a `folly::Function`, just declare it as that! More often than not you will find that you do not need the const qualifier. While writing the `folly::Function` implementation, a good set of unit tests had existed before the const function types got introduced. Not a single one of those unit tests had to be changed: they all compiled and passed after the introduction of const function types. Obviously new ones were added to test the const-correctness. But in your day-to-day use of `folly::Function` you won’t have to worry about const very often.
Memory usage
============
Like most implementations of `std::function`, `folly::Function` stores small callable objects in-line and larger callable objects will be stored on the heap. `folly::Function` returns the size of the allocation it has made from its `heapAllocatedMemory()` member function; naturally it will return `0` when the callable is stored in-line.
......@@ -208,7 +208,7 @@ TEST(Function, InvokeFunctor) {
Function<int(size_t) const> getter = std::move(func);
// Function will allocate memory on the heap to store the functor object
EXPECT_TRUE(getter.hasAllocatedMemory());
EXPECT_GT(getter.heapAllocatedMemory(), 0);
EXPECT_EQ(123, getter(5));
}
......@@ -407,7 +407,7 @@ TEST(Function, NonCopyableLambda) {
EXPECT_EQ(901, functor());
Function<int(void)> func = std::move(functor);
EXPECT_TRUE(func.hasAllocatedMemory());
EXPECT_GT(func.heapAllocatedMemory(), 0);
EXPECT_EQ(902, func());
}
......@@ -1100,11 +1100,11 @@ TEST(Function, NoAllocatedMemoryAfterMove) {
Functor<int, 100> foo;
Function<int(size_t)> func = foo;
EXPECT_TRUE(func.hasAllocatedMemory());
EXPECT_GT(func.heapAllocatedMemory(), 0);
Function<int(size_t)> func2 = std::move(func);
EXPECT_TRUE(func2.hasAllocatedMemory());
EXPECT_FALSE(func.hasAllocatedMemory());
EXPECT_GT(func2.heapAllocatedMemory(), 0);
EXPECT_EQ(func.heapAllocatedMemory(), 0);
}
TEST(Function, ConstCastEmbedded) {
......@@ -1112,10 +1112,10 @@ TEST(Function, ConstCastEmbedded) {
auto functor = [&x]() { ++x; };
Function<void() const> func(functor);
EXPECT_FALSE(func.hasAllocatedMemory());
EXPECT_EQ(func.heapAllocatedMemory(), 0);
Function<void()> func2(std::move(func));
EXPECT_FALSE(func2.hasAllocatedMemory());
EXPECT_EQ(func2.heapAllocatedMemory(), 0);
}
TEST(Function, EmptyAfterConstCast) {
......@@ -1213,3 +1213,21 @@ TEST(Function, MaxAlignCallable) {
EXPECT_EQ(0, f()) << "sanity";
EXPECT_EQ(0, Function<size_t()>(f)());
}
TEST(Function, AllocatedSize) {
Function<void(int)> defaultConstructed;
EXPECT_EQ(defaultConstructed.heapAllocatedMemory(), 0U)
<< "Default constructed Function should have zero allocations";
// On any platform this has to allocate heap storage, because the captures are
// larger than the inline size of the Function object:
constexpr size_t kCaptureBytes = sizeof(Function<void(int)>) + 1;
Function<void(int)> fromLambda{
[x = std::array<char, kCaptureBytes>()](int) { (void)x; }};
// I can't assert much about the size because it's permitted to vary from
// platform to platform or as optimization levels change, but we can be sure
// that the lambda must be at least as large as its captures
EXPECT_GE(fromLambda.heapAllocatedMemory(), kCaptureBytes)
<< "Lambda-derived Function's allocated size is smaller than the "
"lambda's capture size";
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment