Skip to content

Commit b776cf6

Browse files
authoredMar 1, 2025··
Optimize text_style using bit packing (#4363)
1 parent bdbf957 commit b776cf6

File tree

4 files changed

+173
-91
lines changed

4 files changed

+173
-91
lines changed
 

‎doc/api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ performance bottleneck.
580580

581581
`fmt/color.h` provides support for terminal color and text style output.
582582

583-
::: print(const text_style&, format_string<T...>, T&&...)
583+
::: print(text_style, format_string<T...>, T&&...)
584584

585585
::: fg(detail::color_type)
586586

‎include/fmt/color.h

+111-84
Original file line numberDiff line numberDiff line change
@@ -205,121 +205,155 @@ struct rgb {
205205

206206
namespace detail {
207207

208-
// color is a struct of either a rgb color or a terminal color.
208+
// a bit-packed variant of an RGB color, a terminal color, or unset color.
209+
// see text_style for the bit-packing scheme.
209210
struct color_type {
210-
FMT_CONSTEXPR color_type() noexcept : is_rgb(), value{} {}
211-
FMT_CONSTEXPR color_type(color rgb_color) noexcept : is_rgb(true), value{} {
212-
value.rgb_color = static_cast<uint32_t>(rgb_color);
213-
}
214-
FMT_CONSTEXPR color_type(rgb rgb_color) noexcept : is_rgb(true), value{} {
215-
value.rgb_color = (static_cast<uint32_t>(rgb_color.r) << 16) |
216-
(static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b;
217-
}
211+
FMT_CONSTEXPR color_type() noexcept = default;
212+
FMT_CONSTEXPR color_type(color rgb_color) noexcept
213+
: value_(static_cast<uint32_t>(rgb_color) | (1 << 24)) {}
214+
FMT_CONSTEXPR color_type(rgb rgb_color) noexcept
215+
: color_type(static_cast<color>(
216+
(static_cast<uint32_t>(rgb_color.r) << 16) |
217+
(static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b)) {}
218218
FMT_CONSTEXPR color_type(terminal_color term_color) noexcept
219-
: is_rgb(), value{} {
220-
value.term_color = static_cast<uint8_t>(term_color);
219+
: value_(static_cast<uint32_t>(term_color) | (3 << 24)) {}
220+
221+
FMT_CONSTEXPR auto is_terminal_color() const noexcept -> bool {
222+
return (value_ & (1 << 25)) != 0;
223+
}
224+
225+
FMT_CONSTEXPR auto value() const noexcept -> uint32_t {
226+
return value_ & 0xFFFFFF;
221227
}
222-
bool is_rgb;
223-
union color_union {
224-
uint8_t term_color;
225-
uint32_t rgb_color;
226-
} value;
228+
229+
FMT_CONSTEXPR color_type(uint32_t value) noexcept : value_(value) {}
230+
231+
uint32_t value_{};
227232
};
228233
} // namespace detail
229234

230235
/// A text style consisting of foreground and background colors and emphasis.
231236
class text_style {
237+
// The information is packed as follows:
238+
// ┌──┐
239+
// │ 0│─┐
240+
// │..│ ├── foreground color value
241+
// │23│─┘
242+
// ├──┤
243+
// │24│─┬── discriminator for the above value. 00 if unset, 01 if it's
244+
// │25│─┘ an RGB color, or 11 if it's a terminal color (10 is unused)
245+
// ├──┤
246+
// │26│──── overflow bit, always zero (see below)
247+
// ├──┤
248+
// │27│─┐
249+
// │..│ │
250+
// │50│ │
251+
// ├──┤ │
252+
// │51│ ├── background color (same format as the foreground color)
253+
// │52│ │
254+
// ├──┤ │
255+
// │53│─┘
256+
// ├──┤
257+
// │54│─┐
258+
// │..│ ├── emphases
259+
// │61│─┘
260+
// ├──┤
261+
// │62│─┬── unused
262+
// │63│─┘
263+
// └──┘
264+
// The overflow bits are there to make operator|= efficient.
265+
// When ORing, we must throw if, for either the foreground or background,
266+
// one style specifies a terminal color and the other specifies any color
267+
// (terminal or RGB); in other words, if one discriminator is 11 and the
268+
// other is 11 or 01.
269+
//
270+
// We do that check by adding the styles. Consider what adding does to each
271+
// possible pair of discriminators:
272+
// 00 + 00 = 000
273+
// 01 + 00 = 001
274+
// 11 + 00 = 011
275+
// 01 + 01 = 010
276+
// 11 + 01 = 100 (!!)
277+
// 11 + 11 = 110 (!!)
278+
// In the last two cases, the ones we want to catch, the third bit——the
279+
// overflow bit——is set. Bingo.
280+
//
281+
// We must take into account the possible carry bit from the bits
282+
// before the discriminator. The only potentially problematic case is
283+
// 11 + 00 = 011 (a carry bit would make it 100, not good!), but a carry
284+
// bit is impossible in that case, because 00 (unset color) means the
285+
// 24 bits that precede the discriminator are all zero.
286+
//
287+
// This test can be applied to both colors simultaneously.
288+
232289
public:
233290
FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept
234-
: set_foreground_color(), set_background_color(), ems(em) {}
235-
236-
FMT_CONSTEXPR auto operator|=(const text_style& rhs) -> text_style& {
237-
if (!set_foreground_color) {
238-
set_foreground_color = rhs.set_foreground_color;
239-
foreground_color = rhs.foreground_color;
240-
} else if (rhs.set_foreground_color) {
241-
if (!foreground_color.is_rgb || !rhs.foreground_color.is_rgb)
242-
report_error("can't OR a terminal color");
243-
foreground_color.value.rgb_color |= rhs.foreground_color.value.rgb_color;
244-
}
291+
: style_(static_cast<uint64_t>(em) << 54) {}
245292

246-
if (!set_background_color) {
247-
set_background_color = rhs.set_background_color;
248-
background_color = rhs.background_color;
249-
} else if (rhs.set_background_color) {
250-
if (!background_color.is_rgb || !rhs.background_color.is_rgb)
251-
report_error("can't OR a terminal color");
252-
background_color.value.rgb_color |= rhs.background_color.value.rgb_color;
253-
}
254-
255-
ems = static_cast<emphasis>(static_cast<uint8_t>(ems) |
256-
static_cast<uint8_t>(rhs.ems));
293+
FMT_CONSTEXPR auto operator|=(text_style rhs) -> text_style& {
294+
if (((style_ + rhs.style_) & ((1ULL << 26) | (1ULL << 53))) != 0)
295+
report_error("can't OR a terminal color");
296+
style_ |= rhs.style_;
257297
return *this;
258298
}
259299

260-
friend FMT_CONSTEXPR auto operator|(text_style lhs, const text_style& rhs)
300+
friend FMT_CONSTEXPR auto operator|(text_style lhs, text_style rhs)
261301
-> text_style {
262302
return lhs |= rhs;
263303
}
264304

305+
FMT_CONSTEXPR auto operator==(text_style rhs) const noexcept -> bool {
306+
return style_ == rhs.style_;
307+
}
308+
309+
FMT_CONSTEXPR auto operator!=(text_style rhs) const noexcept -> bool {
310+
return !(*this == rhs);
311+
}
312+
265313
FMT_CONSTEXPR auto has_foreground() const noexcept -> bool {
266-
return set_foreground_color;
314+
return (style_ & (1 << 24)) != 0;
267315
}
268316
FMT_CONSTEXPR auto has_background() const noexcept -> bool {
269-
return set_background_color;
317+
return (style_ & (1ULL << 51)) != 0;
270318
}
271319
FMT_CONSTEXPR auto has_emphasis() const noexcept -> bool {
272-
return static_cast<uint8_t>(ems) != 0;
320+
return (style_ >> 54) != 0;
273321
}
274322
FMT_CONSTEXPR auto get_foreground() const noexcept -> detail::color_type {
275323
FMT_ASSERT(has_foreground(), "no foreground specified for this style");
276-
return foreground_color;
324+
return style_ & 0x3FFFFFF;
277325
}
278326
FMT_CONSTEXPR auto get_background() const noexcept -> detail::color_type {
279327
FMT_ASSERT(has_background(), "no background specified for this style");
280-
return background_color;
328+
return (style_ >> 27) & 0x3FFFFFF;
281329
}
282330
FMT_CONSTEXPR auto get_emphasis() const noexcept -> emphasis {
283331
FMT_ASSERT(has_emphasis(), "no emphasis specified for this style");
284-
return ems;
332+
return static_cast<emphasis>(style_ >> 54);
285333
}
286334

287335
private:
288-
FMT_CONSTEXPR text_style(bool is_foreground,
289-
detail::color_type text_color) noexcept
290-
: set_foreground_color(), set_background_color(), ems() {
291-
if (is_foreground) {
292-
foreground_color = text_color;
293-
set_foreground_color = true;
294-
} else {
295-
background_color = text_color;
296-
set_background_color = true;
297-
}
298-
}
336+
FMT_CONSTEXPR text_style(uint64_t style) noexcept : style_(style) {}
299337

300338
friend FMT_CONSTEXPR auto fg(detail::color_type foreground) noexcept
301339
-> text_style;
302340

303341
friend FMT_CONSTEXPR auto bg(detail::color_type background) noexcept
304342
-> text_style;
305343

306-
detail::color_type foreground_color;
307-
detail::color_type background_color;
308-
bool set_foreground_color;
309-
bool set_background_color;
310-
emphasis ems;
344+
uint64_t style_{};
311345
};
312346

313347
/// Creates a text style from the foreground (text) color.
314348
FMT_CONSTEXPR inline auto fg(detail::color_type foreground) noexcept
315349
-> text_style {
316-
return text_style(true, foreground);
350+
return foreground.value_;
317351
}
318352

319353
/// Creates a text style from the background color.
320354
FMT_CONSTEXPR inline auto bg(detail::color_type background) noexcept
321355
-> text_style {
322-
return text_style(false, background);
356+
return static_cast<uint64_t>(background.value_) << 27;
323357
}
324358

325359
FMT_CONSTEXPR inline auto operator|(emphasis lhs, emphasis rhs) noexcept
@@ -334,9 +368,9 @@ template <typename Char> struct ansi_color_escape {
334368
const char* esc) noexcept {
335369
// If we have a terminal color, we need to output another escape code
336370
// sequence.
337-
if (!text_color.is_rgb) {
371+
if (text_color.is_terminal_color()) {
338372
bool is_background = esc == string_view("\x1b[48;2;");
339-
uint32_t value = text_color.value.term_color;
373+
uint32_t value = text_color.value();
340374
// Background ASCII codes are the same as the foreground ones but with
341375
// 10 more.
342376
if (is_background) value += 10u;
@@ -360,7 +394,7 @@ template <typename Char> struct ansi_color_escape {
360394
for (int i = 0; i < 7; i++) {
361395
buffer[i] = static_cast<Char>(esc[i]);
362396
}
363-
rgb color(text_color.value.rgb_color);
397+
rgb color(text_color.value());
364398
to_esc(color.r, buffer + 7, ';');
365399
to_esc(color.g, buffer + 11, ';');
366400
to_esc(color.b, buffer + 15, 'm');
@@ -441,32 +475,26 @@ template <typename T> struct styled_arg : view {
441475
};
442476

443477
template <typename Char>
444-
void vformat_to(buffer<Char>& buf, const text_style& ts,
445-
basic_string_view<Char> fmt,
478+
void vformat_to(buffer<Char>& buf, text_style ts, basic_string_view<Char> fmt,
446479
basic_format_args<buffered_context<Char>> args) {
447-
bool has_style = false;
448480
if (ts.has_emphasis()) {
449-
has_style = true;
450481
auto emphasis = make_emphasis<Char>(ts.get_emphasis());
451482
buf.append(emphasis.begin(), emphasis.end());
452483
}
453484
if (ts.has_foreground()) {
454-
has_style = true;
455485
auto foreground = make_foreground_color<Char>(ts.get_foreground());
456486
buf.append(foreground.begin(), foreground.end());
457487
}
458488
if (ts.has_background()) {
459-
has_style = true;
460489
auto background = make_background_color<Char>(ts.get_background());
461490
buf.append(background.begin(), background.end());
462491
}
463492
vformat_to(buf, fmt, args);
464-
if (has_style) reset_color<Char>(buf);
493+
if (ts != text_style{}) reset_color<Char>(buf);
465494
}
466495
} // namespace detail
467496

468-
inline void vprint(FILE* f, const text_style& ts, string_view fmt,
469-
format_args args) {
497+
inline void vprint(FILE* f, text_style ts, string_view fmt, format_args args) {
470498
auto buf = memory_buffer();
471499
detail::vformat_to(buf, ts, fmt, args);
472500
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,
482510
* "Elapsed time: {0:.2f} seconds", 1.23);
483511
*/
484512
template <typename... T>
485-
void print(FILE* f, const text_style& ts, format_string<T...> fmt,
486-
T&&... args) {
513+
void print(FILE* f, text_style ts, format_string<T...> fmt, T&&... args) {
487514
vprint(f, ts, fmt.str, vargs<T...>{{args...}});
488515
}
489516

@@ -497,11 +524,11 @@ void print(FILE* f, const text_style& ts, format_string<T...> fmt,
497524
* "Elapsed time: {0:.2f} seconds", 1.23);
498525
*/
499526
template <typename... T>
500-
void print(const text_style& ts, format_string<T...> fmt, T&&... args) {
527+
void print(text_style ts, format_string<T...> fmt, T&&... args) {
501528
return print(stdout, ts, fmt, std::forward<T>(args)...);
502529
}
503530

504-
inline auto vformat(const text_style& ts, string_view fmt, format_args args)
531+
inline auto vformat(text_style ts, string_view fmt, format_args args)
505532
-> std::string {
506533
auto buf = memory_buffer();
507534
detail::vformat_to(buf, ts, fmt, args);
@@ -521,16 +548,16 @@ inline auto vformat(const text_style& ts, string_view fmt, format_args args)
521548
* ```
522549
*/
523550
template <typename... T>
524-
inline auto format(const text_style& ts, format_string<T...> fmt, T&&... args)
551+
inline auto format(text_style ts, format_string<T...> fmt, T&&... args)
525552
-> std::string {
526553
return fmt::vformat(ts, fmt.str, vargs<T...>{{args...}});
527554
}
528555

529556
/// Formats a string with the given text_style and writes the output to `out`.
530557
template <typename OutputIt,
531558
FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
532-
auto vformat_to(OutputIt out, const text_style& ts, string_view fmt,
533-
format_args args) -> OutputIt {
559+
auto vformat_to(OutputIt out, text_style ts, string_view fmt, format_args args)
560+
-> OutputIt {
534561
auto&& buf = detail::get_buffer<char>(out);
535562
detail::vformat_to(buf, ts, fmt, args);
536563
return detail::get_iterator(buf, out);
@@ -548,8 +575,8 @@ auto vformat_to(OutputIt out, const text_style& ts, string_view fmt,
548575
*/
549576
template <typename OutputIt, typename... T,
550577
FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
551-
inline auto format_to(OutputIt out, const text_style& ts,
552-
format_string<T...> fmt, T&&... args) -> OutputIt {
578+
inline auto format_to(OutputIt out, text_style ts, format_string<T...> fmt,
579+
T&&... args) -> OutputIt {
553580
return vformat_to(out, ts, fmt.str, vargs<T...>{{args...}});
554581
}
555582

‎include/fmt/xchar.h

+5-5
Original file line numberDiff line numberDiff line change
@@ -322,27 +322,27 @@ template <typename... T> void println(wformat_string<T...> fmt, T&&... args) {
322322
return print(L"{}\n", fmt::format(fmt, std::forward<T>(args)...));
323323
}
324324

325-
inline auto vformat(const text_style& ts, wstring_view fmt, wformat_args args)
325+
inline auto vformat(text_style ts, wstring_view fmt, wformat_args args)
326326
-> std::wstring {
327327
auto buf = wmemory_buffer();
328328
detail::vformat_to(buf, ts, fmt, args);
329329
return {buf.data(), buf.size()};
330330
}
331331

332332
template <typename... T>
333-
inline auto format(const text_style& ts, wformat_string<T...> fmt, T&&... args)
333+
inline auto format(text_style ts, wformat_string<T...> fmt, T&&... args)
334334
-> std::wstring {
335335
return fmt::vformat(ts, fmt, fmt::make_wformat_args(args...));
336336
}
337337

338338
template <typename... T>
339-
FMT_DEPRECATED void print(std::FILE* f, const text_style& ts,
340-
wformat_string<T...> fmt, const T&... args) {
339+
FMT_DEPRECATED void print(std::FILE* f, text_style ts, wformat_string<T...> fmt,
340+
const T&... args) {
341341
vprint(f, ts, fmt, fmt::make_wformat_args(args...));
342342
}
343343

344344
template <typename... T>
345-
FMT_DEPRECATED void print(const text_style& ts, wformat_string<T...> fmt,
345+
FMT_DEPRECATED void print(text_style ts, wformat_string<T...> fmt,
346346
const T&... args) {
347347
return print(stdout, ts, fmt, args...);
348348
}

‎test/color-test.cc

+56-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,66 @@
99

1010
#include <iterator> // std::back_inserter
1111

12-
#include "gtest-extra.h" // EXPECT_WRITE
12+
#include "gtest-extra.h" // EXPECT_WRITE, EXPECT_THROW_MSG
13+
14+
TEST(color_test, text_style) {
15+
EXPECT_FALSE(fmt::text_style{}.has_foreground());
16+
EXPECT_FALSE(fmt::text_style{}.has_background());
17+
EXPECT_FALSE(fmt::text_style{}.has_emphasis());
18+
19+
EXPECT_TRUE(fg(fmt::rgb(0)).has_foreground());
20+
EXPECT_FALSE(fg(fmt::rgb(0)).has_background());
21+
EXPECT_FALSE(fg(fmt::rgb(0)).has_emphasis());
22+
EXPECT_TRUE(bg(fmt::rgb(0)).has_background());
23+
EXPECT_FALSE(bg(fmt::rgb(0)).has_foreground());
24+
EXPECT_FALSE(bg(fmt::rgb(0)).has_emphasis());
25+
26+
EXPECT_TRUE(
27+
(fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_foreground());
28+
EXPECT_TRUE(
29+
(fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_background());
30+
EXPECT_FALSE(
31+
(fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_emphasis());
32+
33+
EXPECT_EQ(fg(fmt::rgb(0x000000)) | fg(fmt::rgb(0x000000)),
34+
fg(fmt::rgb(0x000000)));
35+
EXPECT_EQ(fg(fmt::rgb(0x00000F)) | fg(fmt::rgb(0x00000F)),
36+
fg(fmt::rgb(0x00000F)));
37+
EXPECT_EQ(fg(fmt::rgb(0xC0F000)) | fg(fmt::rgb(0x000FEE)),
38+
fg(fmt::rgb(0xC0FFEE)));
39+
40+
EXPECT_THROW_MSG(
41+
fg(fmt::terminal_color::black) | fg(fmt::terminal_color::black),
42+
fmt::format_error, "can't OR a terminal color");
43+
EXPECT_THROW_MSG(
44+
fg(fmt::terminal_color::black) | fg(fmt::terminal_color::white),
45+
fmt::format_error, "can't OR a terminal color");
46+
EXPECT_THROW_MSG(
47+
bg(fmt::terminal_color::black) | bg(fmt::terminal_color::black),
48+
fmt::format_error, "can't OR a terminal color");
49+
EXPECT_THROW_MSG(
50+
bg(fmt::terminal_color::black) | bg(fmt::terminal_color::white),
51+
fmt::format_error, "can't OR a terminal color");
52+
EXPECT_THROW_MSG(fg(fmt::terminal_color::black) | fg(fmt::color::black),
53+
fmt::format_error, "can't OR a terminal color");
54+
EXPECT_THROW_MSG(bg(fmt::terminal_color::black) | bg(fmt::color::black),
55+
fmt::format_error, "can't OR a terminal color");
56+
57+
EXPECT_NO_THROW(fg(fmt::terminal_color::white) |
58+
bg(fmt::terminal_color::white));
59+
EXPECT_NO_THROW(fg(fmt::terminal_color::white) | bg(fmt::rgb(0xFFFFFF)));
60+
EXPECT_NO_THROW(fg(fmt::terminal_color::white) | fmt::text_style{});
61+
EXPECT_NO_THROW(bg(fmt::terminal_color::white) | fmt::text_style{});
62+
}
1363

1464
TEST(color_test, format) {
65+
EXPECT_EQ(fmt::format(fmt::text_style{}, "no style"), "no style");
1566
EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 20, 30)), "rgb(255,20,30)"),
1667
"\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m");
68+
EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 0, 0)) | fg(fmt::rgb(0, 20, 30)), "rgb(255,20,30)"),
69+
"\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m");
70+
EXPECT_EQ(fmt::format(fg(fmt::rgb(0, 0, 0)) | fg(fmt::rgb(0, 0, 0)), "rgb(0,0,0)"),
71+
"\x1b[38;2;000;000;000mrgb(0,0,0)\x1b[0m");
1772
EXPECT_EQ(fmt::format(fg(fmt::color::blue), "blue"),
1873
"\x1b[38;2;000;000;255mblue\x1b[0m");
1974
EXPECT_EQ(

0 commit comments

Comments
 (0)
Please sign in to comment.