-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Add support for raw gather buffer formatting #1271
Comments
And Networking TS. And Beast. And anything which takes raw gather buffers. If you need to be convinced of the power of this design pattern, consider {fmt} as the backend for a binary logger: binary_logger &log;
log.write("System error code = {}\n", errno); This appends the logged items as binary to the logger. Such a logger would be a ring buffer stored in a To later convert binary logged items into a text representation: llfio::mapped_file_handle &fh; // opened append-only
for(auto &item : log)
{
fh.write(0, fmt::out(item));
} I vaguely remember that you intended to split the constexpr string format parser into a standalone library. Such a layer could parse the binary log statements for {fmt} correctness, and emit the gather buffers list as a list of variants to the binary logger, or perhaps something more compact. That gets appended to the logger mapped file. I guess what I'm actually saying here is that the true power of this design pattern is polymorphic gather buffer formatting, rather than gather buffers of byte arrays. Then you can "squirrel away" a {fmt} into an efficient intermediate representation in deterministic time, and later complete the {fmt} at a time when indeterministic execution time is okay. I hope I have explained myself okay. It is Friday, and I have a chest infection, so rather looking forward to end of work day. It's been a long day. |
@ned14, what will be the difference between
Not a separate library - I am planning to make it part of the public API. Currently it's internal to {fmt}. |
You may think that i/o would be a lot more expensive than Avoiding dynamic memory allocation may seem a bit niche, but I would regularly work on codebases where a link time check checks for any use of dynamic memory allocation, and it fails the build. Thus if {fmt} relies on dynamic memory allocation, people in those codebases can never use it. Edit: Copy and pasting notes from Reddit discussion thread here:
For input of type We thus can store that individual buffer internally in the type returned by
Does this make more sense?
Partially. But remember gather buffers themselves can contain dynamic length. So, if you format a string with a Me personally, I'd restrict |
Out of curiosity, isn't possible to plug in LLFIO and {fmt} with an output iterator and Very likely incorrect code: llfio::file_handle &fh;
fmt::format_to(back_inserter_at_offset(fh, offset),
"System error code = {}\n", errno)); I guess while it may be possible, it would inefficient, right? |
Any buffering is best implemented by higher level code, for example forthcoming Ranges i/o, you see. There Ranges i/o might keep two 4Kb buffers per i/o stream, and double buffer each with |
Could you point me to the documentation of the gather buffer API to help me understand what exactly
It is possible to avoid dynamic memory allocation with fmt::memory_buffer buf;
fmt::format_to(buf, "System error code = {}\n", errno);
fh.write(buf); // possibly need to adapt buf to whatever write expects doesn't do any memory allocations because the output fits into the inline storage of
Decent compilers replace integer division by a constant with an integer multiplication (although that is not very fast either) and {fmt} further minimizes the number of such costly operations.
With {fmt} you can avoid memory allocations. However, the output can be arbitrarily large and not known at compile time, so in general to avoid allocation you'd need to truncate or severely limit the inputs. There are parts of the library that use memory allocation and I am not interested in making such linkage work on those niche systems, but would accept a PR if it's not too intrusive.
This is only true in trivial cases. If you specify width or precision (for FP) then the output can be arbitrarily large.
I think this is a non-starter. We shouldn't be butchering the formatting API for some esoteric use case. At most we should provide an infra to make it possible to do it themselves. |
I quickly threw together a mockup at https://wandbox.org/permlink/l3bSQTYBZDOvd92D.
Can you give an example of where formatting would have an unbounded length temporary? Actually, lest wandbox vanish, better to copy & paste it here: #include <variant>
#include <string>
#include <tuple>
#include <memory>
#include <array>
#include <iostream>
#include <sys/uio.h>
#include <string.h>
struct const_buffer_type { const char *data; size_t len; };
template<class T> struct out_holder_item;
template<> struct out_holder_item<int>
{
const int &value;
char _buffer[11];
size_t _length{0};
out_holder_item(const int &v) : value(v) {}
const_buffer_type render() {
_length = sprintf(_buffer, "%d", value);
return get();
}
const_buffer_type get() const { return {_buffer, _length};}
};
template<> struct out_holder_item<std::string>
{
const std::string &value;
out_holder_item(const std::string &v) : value(v) {}
const_buffer_type render() { return get(); }
const_buffer_type get() const { return { value.c_str(), value.size()};}
};
template<> struct out_holder_item<const char *>
{
const char * value;
size_t _length{0};
out_holder_item(const char * v) : value(v) {}
const_buffer_type render() { _length = strlen(value); return get(); }
const_buffer_type get() const { return { value, _length};}
};
template<class... Args> using snapshot_holder = std::tuple<out_holder_item<Args>...>;
template<class Array, class... Args, std::size_t... Is> void render(Array &buffers, snapshot_holder<Args...> &snapshot, std::index_sequence<Is...>)
{
((buffers[Is] = std::get<Is>(snapshot).render()), ...);
}
template<class Arg> snapshot_holder<std::decay_t<Arg>> snapshot(Arg&& arg)
{
return snapshot_holder<std::decay_t<Arg>>(out_holder_item<std::decay_t<Arg>>(std::forward<Arg>(arg)));
}
template<class Arg, class... Args> snapshot_holder<std::decay_t<Arg>, std::decay_t<Args>...> snapshot(Arg&&arg, Args&&... args)
{
return std::tuple_cat(snapshot(std::forward<Arg>(arg)), snapshot(std::forward<Args>(args)...));
}
template<class...Args> struct out_holder
{
using snapshot_type = snapshot_holder<std::decay_t<Args>...>;
snapshot_type snapshot;
// Gather buffers MUST be contiguous in memory
std::array<const_buffer_type, sizeof...(Args)> buffers;
out_holder(snapshot_type &&s) : snapshot(std::move(s)) {}
const_buffer_type *data() const { return buffers.data();}
size_t size() const { return buffers.size();}
auto begin() const { return buffers.begin(); }
auto end() const { return buffers.end(); }
};
template<class...Args> out_holder<Args...> out(Args&&... args)
{
out_holder<Args...> ret (snapshot(std::forward<Args>(args)...));
render(ret.buffers, ret.snapshot, std::index_sequence_for<Args...>{});
return ret;
}
int main()
{
auto buffers = out("System error code = ", 5, ", and my string is: ", std::string("hello"));
std::cout << buffers.size() << std::endl;
for(auto &buffer : buffers)
{
std::cout << std::string_view(buffer.data, buffer.len);
}
return 0;
} |
@ned14, thanks a lot for the example, it is super helpful. I think we can do something along those lines in {fmt} and avoid memory allocations in almost all cases.
auto s = fmt::format("{:1000}", 42); This will print Thanks for the pointers, @Sarcasm. |
In my example, I poorly tried to split the implementation into three layers. There is a "value recording snapshot layer", which simply dumps what would be formatted. The idea is that a binary logger can deterministically dump what would be formatted very quickly. The next layer
Ah, you've convinced me. If the formatted value exceeds some reasonable bound (256 bytes?), dynamic memory allocation looks very reasonable. |
Not sure if this worth the extra complexity in fmt. |
So to summarize: the suggestion here is to use multiple buffers instead of a single one to avoid copy in some cases (or rather move copy further down the stack). Closing as it will likely be a regression for the common case of usual message formatting and will add significant complexity. The async formatting can be done without any of this. |
This will make {fmt} work with llfio.
From ned14/llfio#32:
The text was updated successfully, but these errors were encountered: