-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Handle allocator propagation in basic_memory_buffer::move, Fix #4487 #4490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
dlex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very good, definitely a move in the right direction. Please see my comments about C++11 compatibility, and a couple about more optimal buffer copying.
include/fmt/format.h
Outdated
| alloc_ = std::move(other.alloc_); | ||
| } else { | ||
| if (alloc_ != other.alloc_) { | ||
| this->reserve(capacity); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be costly to propagate the entire capacity when copying. What if the source buffer used to be 10k, but then shrunk in size to 1k. Its capacity is 10k still. But is there any point in allocating 10k at the target when the actual data to be copied is 1k?
include/fmt/format.h
Outdated
| this->reserve(capacity); | ||
| detail::copy<T>(data, data + size, this->data()); | ||
| this->resize(size); | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch needs to handle the case when data == other.store_ as well. Otherwise the store_ buffer optimization is bypassed for the target buffer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right.
Also, do we need to clear other if data != other.store_?
i.e.
other.set(other.store_, 0);
other.clear();There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say no. Nothing in the standard requires move assignment operator argument to be in a certain state after a successful call, empty or whatever.
include/fmt/format.h
Outdated
|
|
||
| void deallocate(T* p, size_t) { std::free(p); } | ||
|
|
||
| friend bool operator==(const allocator&, const allocator&) noexcept { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's pass allocator by value.
vitaut
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR. Please add a test case and address inline comments.
include/fmt/format.h
Outdated
|
|
||
| friend bool operator==(const allocator&, const allocator&) noexcept { | ||
| return true; // All instances of this allocator are equivalent. | ||
| friend bool operator==(const allocator, const allocator) noexcept { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const is not needed here and below
35b131e to
764fca9
Compare
include/fmt/format.h
Outdated
| // Move data from other to this buffer. | ||
| FMT_CONSTEXPR20 void move(basic_memory_buffer& other) { | ||
| alloc_ = std::move(other.alloc_); | ||
| using alloc_traits = std::allocator_traits<Allocator>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks unused.
include/fmt/format.h
Outdated
| if (alloc_ != other.alloc_ && data != other.store_) { | ||
| size_t size = other.size(), capacity = other.capacity(); | ||
| // Perform copy operation, allocators are different | ||
| this->reserve(size); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICT, basic_memory_buffer::resize calls basic_memory_buffer::try_resize which calls buffer::try_resize, which first calls buffer::try_reserve anyway.
Can we just call this->resize(size) then do the copy instead of making 2 calls?
(I was just browsing through this PR and thought I'd add a comment or two, it looks neat)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification. So, I just need to resize before copying the data, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree to @Arghnews, a call to resize(size) and then to copy seems enough. And we don't care about other.capacity() here BTW.
include/fmt/format.h
Outdated
| this->reserve(size); | ||
| detail::copy<T>(data, data + size, this->data()); | ||
| this->resize(size); | ||
| other.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dlex/your comment flagged this already, but no need to have this clear here, I think
| std::allocator<char>* original_alloc_ptr = buffer.get_allocator().get(); | ||
| basic_memory_buffer<char, 5, std_allocator_n> buffer2( | ||
| std::move(buffer)); | ||
| EXPECT_EQ(str, std::string(&buffer[0], buffer.size())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIU, this check is here to verify that the underlying memory buffer have not been moved from buffer to buffer2, and it's used to verify that memory copying has happened instead. You may want to consider a more black box way to test that: compare a copy of &buffer[0] before the move, and &buffer2[0] after the move, they should be different. And vice versa: where we should expect the underlying buffer has been moved, they should be the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you suggest is a great idea, but I also believe it's essential to verify the contents as well. Since we're copying, it's crucial to ensure the contents remain the same. That said, I will apply your recommendation by adding:
EXPECT_NE(original_data_ptr, new_data_ptr);| const char test[] = "test"; | ||
| buffer.append(string_view(test, 4)); | ||
| check_move_buffer("test", buffer); | ||
| buffer.push_back('a'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buffer has just been moved-from in the 1st invocation of check_move_buffer. I think the test code should not expect that the original data have left in it. It may be better to reassign buffer completely, or use another instance of basic_memory_buffer for the following test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test you're referring to specifically targets the inline buffer, and for that reason, the buffer should remain unchanged. Its purpose is to verify that the content stays the same and that no heap allocation occurs as long as it's within the capacity limit. If you take a look at the move implementation in format.h, you'll see that when an inline buffer is used, the content is simply copied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I understand that buffer sized under small buffer optimizaion threshold is simply copied, but that's not the question.
The question is the same as in the line 366 comment thread above. Which essentially is: is the fact that basic_memory_buffer's buffer keeps the data smaller than the SBO threshold after being moved-from a part of its interface guarantee, or is it an implementation detail?
If it's a part of interface, both these checks are correct and even required.
If it's not, we are testing implementation detail assumptions here which don't have to hold, even though the checks are momentarily correct.
There is no doc about whether it's an interface guarantee, or a comment in the code, that's why I assume the latter. @vitaut has the ultimate say here however.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dlex made a good point.
is the fact that basic_memory_buffer's buffer keeps the data smaller than the SBO threshold after being moved-from a part of its interface guarantee, or is it an implementation detail?
It is definitely an implementation detail and we shouldn't rely on it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this can be done as a follow-up.
test/format-test.cc
Outdated
| buffer.push_back('a'); | ||
| EXPECT_NE(&buffer[0], inline_buffer_ptr); | ||
| std::allocator<char>* original_alloc_ptr = buffer.get_allocator().get(); | ||
| basic_memory_buffer<char, 4, std_allocator_n> buffer2(std::move(buffer)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line highlights another non conformance in basic_memory_buffer not covered in #4487: its move ctor should always initialize its alloc_ from other.alloc_. That can be addressed separately, but for the purpose of this test, it's better to create an empty buffer2 instance, and then move-assign buffer to it.
|
@toprakmurat do you plan to update this PR? |
Yes, I’ll have it ready by the end of this weekend. |
764fca9 to
c77a196
Compare
vitaut
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good but please fix the remaining issues reported in CI.
include/fmt/format.h
Outdated
| allocator_move_impl(basic_memory_buffer& other) { | ||
| T* data = other.data(); | ||
| if (alloc_ != other.alloc_ && data != other.store_) { | ||
| size_t size = other.size(), capacity = other.capacity(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
capacity is unused
Update `basic_memory_buffer::move` to respect `propagate_on_container_move_assignment`allocator trait. If the allocator should not propagate and differs from the target's allocator, fallback to copying the buffer instead of transferring ownership. This avoids potential allocator mismatch issues and ensures exception safety.
- Added two test cases `move_ctor_inline_buffer_non_propagating` and `move_ctor_dynamic_buffer_non_propagating` - Added `PropageteOnMove` template parameter to `allocator_ref` class to be compatible with the old test cases - `allocator_ref` now implements `!=` and `==` operators
c77a196 to
4e219ee
Compare
vitaut
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more CI failures due to (lack of) constexpr.
d6f27f2 to
814f51e
Compare
vitaut
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
Merged, thank you! |
- Update from version 11.2.0 to 12.1.0
- Update of rootfile
- so-bump so mpd requires shipping
- Changelog
12.1.0
- Optimized `buffer::append`, resulting in up to ~16% improvement on spdlog
benchmarks (fmtlib/fmt#4541). Thanks @fyrsta7.
- Worked around an ABI incompatibility in `std::locale_ref` between clang and
gcc (fmtlib/fmt#4573).
- Made `std::variant` and `std::expected` formatters work with `format_as`
(fmtlib/fmt#4574,
fmtlib/fmt#4575). Thanks @phprus.
- Made `fmt::join<string_view>` work with C++ modules
(fmtlib/fmt#4379,
fmtlib/fmt#4577). Thanks @Arghnews.
- Exported `fmt::is_compiled_string` and `operator""_cf` from the module
(fmtlib/fmt#4544). Thanks @CrackedMatter.
- Fixed a compatibility issue with C++ modules in clang
(fmtlib/fmt#4548). Thanks @tsarn.
- Added support for cv-qualified types to the `std::optional` formatter
(fmtlib/fmt#4561,
fmtlib/fmt#4562). Thanks @OleksandrKvl.
- Added demangling support (used in exception and `std::type_info` formatters)
for libc++ and clang-cl
(fmtlib/fmt#4542,
fmtlib/fmt#4560,
fmtlib/fmt#4568,
fmtlib/fmt#4571).
Thanks @FatihBAKIR and @rohitsutreja.
- Switched to global `malloc`/`free` to enable allocator customization
(fmtlib/fmt#4569,
fmtlib/fmt#4570). Thanks @rohitsutreja.
- Made the `FMT_USE_CONSTEVAL` macro configurable by users
(fmtlib/fmt#4546). Thanks @SnapperTT.
- Fixed compilation with locales disabled in the header-only mode
(fmtlib/fmt#4550).
- Fixed compilation with clang 21 and `-std=c++20`
(fmtlib/fmt#4552).
- Fixed a dynamic linking issue with clang-cl
(fmtlib/fmt#4576,
fmtlib/fmt#4584). Thanks @FatihBAKIR.
- Fixed a warning suppression leakage on gcc
(fmtlib/fmt#4588). Thanks @ZedThree.
- Made more internal color APIs `constexpr`
(fmtlib/fmt#4581). Thanks @ishani.
- Fixed compatibility with clang as a host compiler for NVCC
(fmtlib/fmt#4564). Thanks @valgur.
- Fixed various warnings and lint issues
(fmtlib/fmt#4565,
fmtlib/fmt#4572,
fmtlib/fmt#4557).
Thanks @LiangHuDream and @teruyamato0731.
- Improved documentation
(fmtlib/fmt#4549,
fmtlib/fmt#4551,
fmtlib/fmt#4566,
fmtlib/fmt#4567,
fmtlib/fmt#4578,).
Thanks @teruyamato0731, @petersteneteg and @zimmerman-dev.
12.0.0
- Optimized the default floating point formatting
(fmtlib/fmt#3675,
fmtlib/fmt#4516). In particular, formatting a
`double` with format string compilation into a stack allocated buffer is
more than 60% faster in version 12.0 compared to 11.2 according to
[dtoa-benchmark](https://github.com/fmtlib/dtoa-benchmark):
```
Function Time (ns) Speedup
fmt11 34.471 1.00x
fmt12 21.000 1.64x
```
<img width="766" height="609" src="https://github.com/user-attachments/assets/d7d768ad-7543-468c-b0bb-449abf73b31b" />
- Added `constexpr` support to `fmt::format`. For example:
```c++
#include <fmt/compile.h>
using namespace fmt::literals;
std::string s = fmt::format(""_cf, 42);
```
now works at compile time provided that `std::string` supports `constexpr`
(fmtlib/fmt#3403,
fmtlib/fmt#4456). Thanks @msvetkin.
- Added `FMT_STATIC_FORMAT` that allows formatting into a string of the exact
required size at compile time.
For example:
```c++
#include <fmt/compile.h>
constexpr auto s = FMT_STATIC_FORMAT("{}", 42);
```
compiles to just
```s
__ZL1s:
.asciiz "42"
```
It can be accessed as a C string with `s.c_str()` or as a string view with
`s.str()`.
- Improved C++20 module support
(fmtlib/fmt#4451,
fmtlib/fmt#4459,
fmtlib/fmt#4476,
fmtlib/fmt#4488,
fmtlib/fmt#4491,
fmtlib/fmt#4495).
Thanks @arBmind, @tkhyn, @Mishura4, @anonymouspc and @autoantwort.
- Switched to using estimated display width in precision. For example:
```c++
fmt::print("|{:.4}|\n|1234|\n", "🐱🐱🐱");
```
prints

because `🐱` has an estimated width of 2
(fmtlib/fmt#4272,
fmtlib/fmt#4443,
fmtlib/fmt#4475).
Thanks @nikhilreddydev and @localspook.
- Fix interaction between debug presentation, precision, and width for strings
(fmtlib/fmt#4478). Thanks @localspook.
- Implemented allocator propagation on `basic_memory_buffer` move
(fmtlib/fmt#4487,
fmtlib/fmt#4490). Thanks @toprakmurat.
- Fixed an ambiguity between `std::reference_wrapper<T>` and `format_as`
formatters (fmtlib/fmt#4424,
fmtlib/fmt#4434). Thanks @jeremy-rifkin.
- Removed the following deprecated APIs:
- `has_formatter`: use `is_formattable` instead,
- `basic_format_args::parse_context_type`,
`basic_format_args::formatter_type` and similar aliases in context types,
- wide stream overload of `fmt::printf`,
- wide stream overloads of `fmt::print` that take text styles,
- `is_*char` traits,
- `fmt::localtime`.
- Deprecated wide overloads of `fmt::fprintf` and `fmt::sprintf`.
- Improved diagnostics for the incorrect usage of `fmt::ptr`
(fmtlib/fmt#4453). Thanks @TobiSchluter.
- Made handling of ANSI escape sequences more efficient
(fmtlib/fmt#4511,
fmtlib/fmt#4528).
Thanks @localspook and @Anas-Hamdane.
- Fixed a buffer overflow on all emphasis flags set
(fmtlib/fmt#4498). Thanks @dominicpoeschko.
- Fixed an integer overflow for precision close to the max `int` value.
- Fixed compatibility with WASI (fmtlib/fmt#4496,
fmtlib/fmt#4497). Thanks @whitequark.
- Fixed `back_insert_iterator` detection, preventing a fallback on slower path
that handles arbitrary iterators (fmtlib/fmt#4454).
- Fixed handling of invalid glibc `FILE` buffers
(fmtlib/fmt#4469).
- Added `wchar_t` support to the `std::byte` formatter
(fmtlib/fmt#4479,
fmtlib/fmt#4480). Thanks @phprus.
- Changed component prefix from `fmt-` to `fmt_` for compatibility with
NSIS/CPack on Windows, e.g. `fmt-doc` changed to `fmt_doc`
(fmtlib/fmt#4441,
fmtlib/fmt#4442). Thanks @n-stein.
- Added the `FMT_CUSTOM_ASSERT_FAIL` macro to simplify providing a custom
`fmt::assert_fail` implementation (fmtlib/fmt#4505).
Thanks @HazardyKnusperkeks.
- Switched to `FMT_THROW` on reporting format errors so that it can be
overriden by users when exceptions are disabled
(fmtlib/fmt#4521). Thanks @HazardyKnusperkeks.
- Improved master project detection and disabled install targets when using
{fmt} as a subproject by default (fmtlib/fmt#4536).
Thanks @crueter.
- Made various code improvements
(fmtlib/fmt#4445,
fmtlib/fmt#4448,
fmtlib/fmt#4473,
fmtlib/fmt#4522).
Thanks @localspook, @tchaikov and @way4sahil.
- Added Conan instructions to the docs
(fmtlib/fmt#4537). Thanks @uilianries.
- Removed Bazel files to avoid issues with downstream packaging
(fmtlib/fmt#4530). Thanks @mering.
- Added more entries for generated files to `.gitignore`
(fmtlib/fmt#4355,
fmtlib/fmt#4512).
Thanks @dinomight and @localspook.
- Fixed various warnings and compilation issues
(fmtlib/fmt#4447,
fmtlib/fmt#4470,
fmtlib/fmt#4474,
fmtlib/fmt#4477,
fmtlib/fmt#4471,
fmtlib/fmt#4483,
fmtlib/fmt#4515,
fmtlib/fmt#4533,
fmtlib/fmt#4534).
Thanks @dodomorandi, @localspook, @remyjette, @Tomek-Stolarczyk, @Mishura4,
@mattiasljungstrom and @FatihBAKIR.
Signed-off-by: Adolf Belka <[email protected]>
Signed-off-by: Michael Tremer <[email protected]>
Fixes #4487.
This PR implements allocator-aware move behavior in
basic_memory_buffer, following the suggestion by @dlex in the linked issue.To enable allocator comparison (
alloc_ == other.alloc_), I addedoperator==andoperator!=to the following classes:allocator_refallocatorSummary of Changes
std::allocator_traits<>::propagate_on_container_move_assignment.Test Failures
There are currently two failing tests (Test 7/21) that may need to be updated to match the new move semantics:
move_ctor_inline_buffermove_ctor_dynamic_bufferI propose updating the above tests to align with the allocator propagation semantics. However, before proceeding, I’d like to get feedback from maintainers and reviewers. @vitaut
Do these changes and the proposed test adjustments align with the intended design of
basic_memory_buffer?