Optimize text_style using bit packing (#4363)

This commit is contained in:
Victor Chernyakin
2025-03-01 12:18:19 -07:00
committed by GitHub
parent bdbf957b9a
commit b776cf66fc
4 changed files with 173 additions and 91 deletions

View File

@ -580,7 +580,7 @@ performance bottleneck.
`fmt/color.h` provides support for terminal color and text style output. `fmt/color.h` provides support for terminal color and text style output.
::: print(const text_style&, format_string<T...>, T&&...) ::: print(text_style, format_string<T...>, T&&...)
::: fg(detail::color_type) ::: fg(detail::color_type)

View File

@ -205,97 +205,135 @@ struct rgb {
namespace detail { namespace detail {
// color is a struct of either a rgb color or a terminal color. // a bit-packed variant of an RGB color, a terminal color, or unset color.
// see text_style for the bit-packing scheme.
struct color_type { struct color_type {
FMT_CONSTEXPR color_type() noexcept : is_rgb(), value{} {} FMT_CONSTEXPR color_type() noexcept = default;
FMT_CONSTEXPR color_type(color rgb_color) noexcept : is_rgb(true), value{} { FMT_CONSTEXPR color_type(color rgb_color) noexcept
value.rgb_color = static_cast<uint32_t>(rgb_color); : value_(static_cast<uint32_t>(rgb_color) | (1 << 24)) {}
} FMT_CONSTEXPR color_type(rgb rgb_color) noexcept
FMT_CONSTEXPR color_type(rgb rgb_color) noexcept : is_rgb(true), value{} { : color_type(static_cast<color>(
value.rgb_color = (static_cast<uint32_t>(rgb_color.r) << 16) | (static_cast<uint32_t>(rgb_color.r) << 16) |
(static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b; (static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b)) {}
}
FMT_CONSTEXPR color_type(terminal_color term_color) noexcept FMT_CONSTEXPR color_type(terminal_color term_color) noexcept
: is_rgb(), value{} { : value_(static_cast<uint32_t>(term_color) | (3 << 24)) {}
value.term_color = static_cast<uint8_t>(term_color);
FMT_CONSTEXPR auto is_terminal_color() const noexcept -> bool {
return (value_ & (1 << 25)) != 0;
} }
bool is_rgb;
union color_union { FMT_CONSTEXPR auto value() const noexcept -> uint32_t {
uint8_t term_color; return value_ & 0xFFFFFF;
uint32_t rgb_color; }
} value;
FMT_CONSTEXPR color_type(uint32_t value) noexcept : value_(value) {}
uint32_t value_{};
}; };
} // namespace detail } // namespace detail
/// A text style consisting of foreground and background colors and emphasis. /// A text style consisting of foreground and background colors and emphasis.
class text_style { class text_style {
// The information is packed as follows:
// ┌──┐
// │ 0│─┐
// │..│ ├── foreground color value
// │23│─┘
// ├──┤
// │24│─┬── discriminator for the above value. 00 if unset, 01 if it's
// │25│─┘ an RGB color, or 11 if it's a terminal color (10 is unused)
// ├──┤
// │26│──── overflow bit, always zero (see below)
// ├──┤
// │27│─┐
// │..│ │
// │50│ │
// ├──┤ │
// │51│ ├── background color (same format as the foreground color)
// │52│ │
// ├──┤ │
// │53│─┘
// ├──┤
// │54│─┐
// │..│ ├── emphases
// │61│─┘
// ├──┤
// │62│─┬── unused
// │63│─┘
// └──┘
// The overflow bits are there to make operator|= efficient.
// When ORing, we must throw if, for either the foreground or background,
// one style specifies a terminal color and the other specifies any color
// (terminal or RGB); in other words, if one discriminator is 11 and the
// other is 11 or 01.
//
// We do that check by adding the styles. Consider what adding does to each
// possible pair of discriminators:
// 00 + 00 = 000
// 01 + 00 = 001
// 11 + 00 = 011
// 01 + 01 = 010
// 11 + 01 = 100 (!!)
// 11 + 11 = 110 (!!)
// In the last two cases, the ones we want to catch, the third bit——the
// overflow bit——is set. Bingo.
//
// We must take into account the possible carry bit from the bits
// before the discriminator. The only potentially problematic case is
// 11 + 00 = 011 (a carry bit would make it 100, not good!), but a carry
// bit is impossible in that case, because 00 (unset color) means the
// 24 bits that precede the discriminator are all zero.
//
// This test can be applied to both colors simultaneously.
public: public:
FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept
: set_foreground_color(), set_background_color(), ems(em) {} : style_(static_cast<uint64_t>(em) << 54) {}
FMT_CONSTEXPR auto operator|=(const text_style& rhs) -> text_style& { FMT_CONSTEXPR auto operator|=(text_style rhs) -> text_style& {
if (!set_foreground_color) { if (((style_ + rhs.style_) & ((1ULL << 26) | (1ULL << 53))) != 0)
set_foreground_color = rhs.set_foreground_color; report_error("can't OR a terminal color");
foreground_color = rhs.foreground_color; style_ |= rhs.style_;
} else if (rhs.set_foreground_color) {
if (!foreground_color.is_rgb || !rhs.foreground_color.is_rgb)
report_error("can't OR a terminal color");
foreground_color.value.rgb_color |= rhs.foreground_color.value.rgb_color;
}
if (!set_background_color) {
set_background_color = rhs.set_background_color;
background_color = rhs.background_color;
} else if (rhs.set_background_color) {
if (!background_color.is_rgb || !rhs.background_color.is_rgb)
report_error("can't OR a terminal color");
background_color.value.rgb_color |= rhs.background_color.value.rgb_color;
}
ems = static_cast<emphasis>(static_cast<uint8_t>(ems) |
static_cast<uint8_t>(rhs.ems));
return *this; return *this;
} }
friend FMT_CONSTEXPR auto operator|(text_style lhs, const text_style& rhs) friend FMT_CONSTEXPR auto operator|(text_style lhs, text_style rhs)
-> text_style { -> text_style {
return lhs |= rhs; return lhs |= rhs;
} }
FMT_CONSTEXPR auto operator==(text_style rhs) const noexcept -> bool {
return style_ == rhs.style_;
}
FMT_CONSTEXPR auto operator!=(text_style rhs) const noexcept -> bool {
return !(*this == rhs);
}
FMT_CONSTEXPR auto has_foreground() const noexcept -> bool { FMT_CONSTEXPR auto has_foreground() const noexcept -> bool {
return set_foreground_color; return (style_ & (1 << 24)) != 0;
} }
FMT_CONSTEXPR auto has_background() const noexcept -> bool { FMT_CONSTEXPR auto has_background() const noexcept -> bool {
return set_background_color; return (style_ & (1ULL << 51)) != 0;
} }
FMT_CONSTEXPR auto has_emphasis() const noexcept -> bool { FMT_CONSTEXPR auto has_emphasis() const noexcept -> bool {
return static_cast<uint8_t>(ems) != 0; return (style_ >> 54) != 0;
} }
FMT_CONSTEXPR auto get_foreground() const noexcept -> detail::color_type { FMT_CONSTEXPR auto get_foreground() const noexcept -> detail::color_type {
FMT_ASSERT(has_foreground(), "no foreground specified for this style"); FMT_ASSERT(has_foreground(), "no foreground specified for this style");
return foreground_color; return style_ & 0x3FFFFFF;
} }
FMT_CONSTEXPR auto get_background() const noexcept -> detail::color_type { FMT_CONSTEXPR auto get_background() const noexcept -> detail::color_type {
FMT_ASSERT(has_background(), "no background specified for this style"); FMT_ASSERT(has_background(), "no background specified for this style");
return background_color; return (style_ >> 27) & 0x3FFFFFF;
} }
FMT_CONSTEXPR auto get_emphasis() const noexcept -> emphasis { FMT_CONSTEXPR auto get_emphasis() const noexcept -> emphasis {
FMT_ASSERT(has_emphasis(), "no emphasis specified for this style"); FMT_ASSERT(has_emphasis(), "no emphasis specified for this style");
return ems; return static_cast<emphasis>(style_ >> 54);
} }
private: private:
FMT_CONSTEXPR text_style(bool is_foreground, FMT_CONSTEXPR text_style(uint64_t style) noexcept : style_(style) {}
detail::color_type text_color) noexcept
: set_foreground_color(), set_background_color(), ems() {
if (is_foreground) {
foreground_color = text_color;
set_foreground_color = true;
} else {
background_color = text_color;
set_background_color = true;
}
}
friend FMT_CONSTEXPR auto fg(detail::color_type foreground) noexcept friend FMT_CONSTEXPR auto fg(detail::color_type foreground) noexcept
-> text_style; -> text_style;
@ -303,23 +341,19 @@ class text_style {
friend FMT_CONSTEXPR auto bg(detail::color_type background) noexcept friend FMT_CONSTEXPR auto bg(detail::color_type background) noexcept
-> text_style; -> text_style;
detail::color_type foreground_color; uint64_t style_{};
detail::color_type background_color;
bool set_foreground_color;
bool set_background_color;
emphasis ems;
}; };
/// Creates a text style from the foreground (text) color. /// Creates a text style from the foreground (text) color.
FMT_CONSTEXPR inline auto fg(detail::color_type foreground) noexcept FMT_CONSTEXPR inline auto fg(detail::color_type foreground) noexcept
-> text_style { -> text_style {
return text_style(true, foreground); return foreground.value_;
} }
/// Creates a text style from the background color. /// Creates a text style from the background color.
FMT_CONSTEXPR inline auto bg(detail::color_type background) noexcept FMT_CONSTEXPR inline auto bg(detail::color_type background) noexcept
-> text_style { -> text_style {
return text_style(false, background); return static_cast<uint64_t>(background.value_) << 27;
} }
FMT_CONSTEXPR inline auto operator|(emphasis lhs, emphasis rhs) noexcept FMT_CONSTEXPR inline auto operator|(emphasis lhs, emphasis rhs) noexcept
@ -334,9 +368,9 @@ template <typename Char> struct ansi_color_escape {
const char* esc) noexcept { const char* esc) noexcept {
// If we have a terminal color, we need to output another escape code // If we have a terminal color, we need to output another escape code
// sequence. // sequence.
if (!text_color.is_rgb) { if (text_color.is_terminal_color()) {
bool is_background = esc == string_view("\x1b[48;2;"); bool is_background = esc == string_view("\x1b[48;2;");
uint32_t value = text_color.value.term_color; uint32_t value = text_color.value();
// Background ASCII codes are the same as the foreground ones but with // Background ASCII codes are the same as the foreground ones but with
// 10 more. // 10 more.
if (is_background) value += 10u; if (is_background) value += 10u;
@ -360,7 +394,7 @@ template <typename Char> struct ansi_color_escape {
for (int i = 0; i < 7; i++) { for (int i = 0; i < 7; i++) {
buffer[i] = static_cast<Char>(esc[i]); buffer[i] = static_cast<Char>(esc[i]);
} }
rgb color(text_color.value.rgb_color); rgb color(text_color.value());
to_esc(color.r, buffer + 7, ';'); to_esc(color.r, buffer + 7, ';');
to_esc(color.g, buffer + 11, ';'); to_esc(color.g, buffer + 11, ';');
to_esc(color.b, buffer + 15, 'm'); to_esc(color.b, buffer + 15, 'm');
@ -441,32 +475,26 @@ template <typename T> struct styled_arg : view {
}; };
template <typename Char> template <typename Char>
void vformat_to(buffer<Char>& buf, const text_style& ts, void vformat_to(buffer<Char>& buf, text_style ts, basic_string_view<Char> fmt,
basic_string_view<Char> fmt,
basic_format_args<buffered_context<Char>> args) { basic_format_args<buffered_context<Char>> args) {
bool has_style = false;
if (ts.has_emphasis()) { if (ts.has_emphasis()) {
has_style = true;
auto emphasis = make_emphasis<Char>(ts.get_emphasis()); auto emphasis = make_emphasis<Char>(ts.get_emphasis());
buf.append(emphasis.begin(), emphasis.end()); buf.append(emphasis.begin(), emphasis.end());
} }
if (ts.has_foreground()) { if (ts.has_foreground()) {
has_style = true;
auto foreground = make_foreground_color<Char>(ts.get_foreground()); auto foreground = make_foreground_color<Char>(ts.get_foreground());
buf.append(foreground.begin(), foreground.end()); buf.append(foreground.begin(), foreground.end());
} }
if (ts.has_background()) { if (ts.has_background()) {
has_style = true;
auto background = make_background_color<Char>(ts.get_background()); auto background = make_background_color<Char>(ts.get_background());
buf.append(background.begin(), background.end()); buf.append(background.begin(), background.end());
} }
vformat_to(buf, fmt, args); vformat_to(buf, fmt, args);
if (has_style) reset_color<Char>(buf); if (ts != text_style{}) reset_color<Char>(buf);
} }
} // namespace detail } // namespace detail
inline void vprint(FILE* f, const text_style& ts, string_view fmt, inline void vprint(FILE* f, text_style ts, string_view fmt, format_args args) {
format_args args) {
auto buf = memory_buffer(); auto buf = memory_buffer();
detail::vformat_to(buf, ts, fmt, args); detail::vformat_to(buf, ts, fmt, args);
print(f, FMT_STRING("{}"), string_view(buf.begin(), buf.size())); print(f, FMT_STRING("{}"), string_view(buf.begin(), buf.size()));
@ -482,8 +510,7 @@ inline void vprint(FILE* f, const text_style& ts, string_view fmt,
* "Elapsed time: {0:.2f} seconds", 1.23); * "Elapsed time: {0:.2f} seconds", 1.23);
*/ */
template <typename... T> template <typename... T>
void print(FILE* f, const text_style& ts, format_string<T...> fmt, void print(FILE* f, text_style ts, format_string<T...> fmt, T&&... args) {
T&&... args) {
vprint(f, ts, fmt.str, vargs<T...>{{args...}}); vprint(f, ts, fmt.str, vargs<T...>{{args...}});
} }
@ -497,11 +524,11 @@ void print(FILE* f, const text_style& ts, format_string<T...> fmt,
* "Elapsed time: {0:.2f} seconds", 1.23); * "Elapsed time: {0:.2f} seconds", 1.23);
*/ */
template <typename... T> template <typename... T>
void print(const text_style& ts, format_string<T...> fmt, T&&... args) { void print(text_style ts, format_string<T...> fmt, T&&... args) {
return print(stdout, ts, fmt, std::forward<T>(args)...); return print(stdout, ts, fmt, std::forward<T>(args)...);
} }
inline auto vformat(const text_style& ts, string_view fmt, format_args args) inline auto vformat(text_style ts, string_view fmt, format_args args)
-> std::string { -> std::string {
auto buf = memory_buffer(); auto buf = memory_buffer();
detail::vformat_to(buf, ts, fmt, args); detail::vformat_to(buf, ts, fmt, args);
@ -521,7 +548,7 @@ inline auto vformat(const text_style& ts, string_view fmt, format_args args)
* ``` * ```
*/ */
template <typename... T> template <typename... T>
inline auto format(const text_style& ts, format_string<T...> fmt, T&&... args) inline auto format(text_style ts, format_string<T...> fmt, T&&... args)
-> std::string { -> std::string {
return fmt::vformat(ts, fmt.str, vargs<T...>{{args...}}); return fmt::vformat(ts, fmt.str, vargs<T...>{{args...}});
} }
@ -529,8 +556,8 @@ inline auto format(const text_style& ts, format_string<T...> fmt, T&&... args)
/// Formats a string with the given text_style and writes the output to `out`. /// Formats a string with the given text_style and writes the output to `out`.
template <typename OutputIt, template <typename OutputIt,
FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)> FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
auto vformat_to(OutputIt out, const text_style& ts, string_view fmt, auto vformat_to(OutputIt out, text_style ts, string_view fmt, format_args args)
format_args args) -> OutputIt { -> OutputIt {
auto&& buf = detail::get_buffer<char>(out); auto&& buf = detail::get_buffer<char>(out);
detail::vformat_to(buf, ts, fmt, args); detail::vformat_to(buf, ts, fmt, args);
return detail::get_iterator(buf, out); return detail::get_iterator(buf, out);
@ -548,8 +575,8 @@ auto vformat_to(OutputIt out, const text_style& ts, string_view fmt,
*/ */
template <typename OutputIt, typename... T, template <typename OutputIt, typename... T,
FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)> FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
inline auto format_to(OutputIt out, const text_style& ts, inline auto format_to(OutputIt out, text_style ts, format_string<T...> fmt,
format_string<T...> fmt, T&&... args) -> OutputIt { T&&... args) -> OutputIt {
return vformat_to(out, ts, fmt.str, vargs<T...>{{args...}}); return vformat_to(out, ts, fmt.str, vargs<T...>{{args...}});
} }

View File

@ -322,7 +322,7 @@ template <typename... T> void println(wformat_string<T...> fmt, T&&... args) {
return print(L"{}\n", fmt::format(fmt, std::forward<T>(args)...)); return print(L"{}\n", fmt::format(fmt, std::forward<T>(args)...));
} }
inline auto vformat(const text_style& ts, wstring_view fmt, wformat_args args) inline auto vformat(text_style ts, wstring_view fmt, wformat_args args)
-> std::wstring { -> std::wstring {
auto buf = wmemory_buffer(); auto buf = wmemory_buffer();
detail::vformat_to(buf, ts, fmt, args); detail::vformat_to(buf, ts, fmt, args);
@ -330,19 +330,19 @@ inline auto vformat(const text_style& ts, wstring_view fmt, wformat_args args)
} }
template <typename... T> template <typename... T>
inline auto format(const text_style& ts, wformat_string<T...> fmt, T&&... args) inline auto format(text_style ts, wformat_string<T...> fmt, T&&... args)
-> std::wstring { -> std::wstring {
return fmt::vformat(ts, fmt, fmt::make_wformat_args(args...)); return fmt::vformat(ts, fmt, fmt::make_wformat_args(args...));
} }
template <typename... T> template <typename... T>
FMT_DEPRECATED void print(std::FILE* f, const text_style& ts, FMT_DEPRECATED void print(std::FILE* f, text_style ts, wformat_string<T...> fmt,
wformat_string<T...> fmt, const T&... args) { const T&... args) {
vprint(f, ts, fmt, fmt::make_wformat_args(args...)); vprint(f, ts, fmt, fmt::make_wformat_args(args...));
} }
template <typename... T> template <typename... T>
FMT_DEPRECATED void print(const text_style& ts, wformat_string<T...> fmt, FMT_DEPRECATED void print(text_style ts, wformat_string<T...> fmt,
const T&... args) { const T&... args) {
return print(stdout, ts, fmt, args...); return print(stdout, ts, fmt, args...);
} }

View File

@ -9,11 +9,66 @@
#include <iterator> // std::back_inserter #include <iterator> // std::back_inserter
#include "gtest-extra.h" // EXPECT_WRITE #include "gtest-extra.h" // EXPECT_WRITE, EXPECT_THROW_MSG
TEST(color_test, text_style) {
EXPECT_FALSE(fmt::text_style{}.has_foreground());
EXPECT_FALSE(fmt::text_style{}.has_background());
EXPECT_FALSE(fmt::text_style{}.has_emphasis());
EXPECT_TRUE(fg(fmt::rgb(0)).has_foreground());
EXPECT_FALSE(fg(fmt::rgb(0)).has_background());
EXPECT_FALSE(fg(fmt::rgb(0)).has_emphasis());
EXPECT_TRUE(bg(fmt::rgb(0)).has_background());
EXPECT_FALSE(bg(fmt::rgb(0)).has_foreground());
EXPECT_FALSE(bg(fmt::rgb(0)).has_emphasis());
EXPECT_TRUE(
(fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_foreground());
EXPECT_TRUE(
(fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_background());
EXPECT_FALSE(
(fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_emphasis());
EXPECT_EQ(fg(fmt::rgb(0x000000)) | fg(fmt::rgb(0x000000)),
fg(fmt::rgb(0x000000)));
EXPECT_EQ(fg(fmt::rgb(0x00000F)) | fg(fmt::rgb(0x00000F)),
fg(fmt::rgb(0x00000F)));
EXPECT_EQ(fg(fmt::rgb(0xC0F000)) | fg(fmt::rgb(0x000FEE)),
fg(fmt::rgb(0xC0FFEE)));
EXPECT_THROW_MSG(
fg(fmt::terminal_color::black) | fg(fmt::terminal_color::black),
fmt::format_error, "can't OR a terminal color");
EXPECT_THROW_MSG(
fg(fmt::terminal_color::black) | fg(fmt::terminal_color::white),
fmt::format_error, "can't OR a terminal color");
EXPECT_THROW_MSG(
bg(fmt::terminal_color::black) | bg(fmt::terminal_color::black),
fmt::format_error, "can't OR a terminal color");
EXPECT_THROW_MSG(
bg(fmt::terminal_color::black) | bg(fmt::terminal_color::white),
fmt::format_error, "can't OR a terminal color");
EXPECT_THROW_MSG(fg(fmt::terminal_color::black) | fg(fmt::color::black),
fmt::format_error, "can't OR a terminal color");
EXPECT_THROW_MSG(bg(fmt::terminal_color::black) | bg(fmt::color::black),
fmt::format_error, "can't OR a terminal color");
EXPECT_NO_THROW(fg(fmt::terminal_color::white) |
bg(fmt::terminal_color::white));
EXPECT_NO_THROW(fg(fmt::terminal_color::white) | bg(fmt::rgb(0xFFFFFF)));
EXPECT_NO_THROW(fg(fmt::terminal_color::white) | fmt::text_style{});
EXPECT_NO_THROW(bg(fmt::terminal_color::white) | fmt::text_style{});
}
TEST(color_test, format) { TEST(color_test, format) {
EXPECT_EQ(fmt::format(fmt::text_style{}, "no style"), "no style");
EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 20, 30)), "rgb(255,20,30)"), EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 20, 30)), "rgb(255,20,30)"),
"\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m"); "\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m");
EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 0, 0)) | fg(fmt::rgb(0, 20, 30)), "rgb(255,20,30)"),
"\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m");
EXPECT_EQ(fmt::format(fg(fmt::rgb(0, 0, 0)) | fg(fmt::rgb(0, 0, 0)), "rgb(0,0,0)"),
"\x1b[38;2;000;000;000mrgb(0,0,0)\x1b[0m");
EXPECT_EQ(fmt::format(fg(fmt::color::blue), "blue"), EXPECT_EQ(fmt::format(fg(fmt::color::blue), "blue"),
"\x1b[38;2;000;000;255mblue\x1b[0m"); "\x1b[38;2;000;000;255mblue\x1b[0m");
EXPECT_EQ( EXPECT_EQ(