網路行銷
{fmt} 格式化函數函式函式庫函數進位佔用空間小而聞名,與 IOStreams、Boost Format 或談論tinyformat 等替代方案相比,每次函數呼叫產生的程式碼通常要小幾倍。應用類型來實現的,從而有效縮小模板膨脹。
統計參數流失類型供貨 format_args
:
auto vformat(string_view fmt, format_args args) -> std::string;template typename... T>auto format(format_stringT...> fmt, T&&... args) -> std::string { return vformat(fmt, fmt::make_format_args(args...));}
如你所見, format
將其所有工作委託給 vformat
,這不是模板。
輸出迭代器和其他輸出類型也可以使用專門設計的肖像API進行類型佈局。
這種方法將模板的使用限制在最小的範圍內,從而實現更小的二進位檔案大小和更快的建置時間。
例如,以下計劃代碼:
// test.cc#include int main() { fmt::print("The answer is {}.", 42);}
編譯為
.LC0: .string "The answer is {}."main: sub rsp, 24 mov eax, 1 mov edi, OFFSET FLAT:.LC0 mov esi, 17 mov rcx, rsp mov rdx, rax mov DWORD PTR [rsp], 42 call fmt::v11::vprint(fmt::v11::basic_string_view, fmt::v11::basic_format_args<:v11::context>) xor eax, eax add rsp, 24 ret
神箭
它比程序相當於 IOStreams 碼小,並且與 printf
:
.LC0: .string "The answer is %d."main: sub rsp, 8 mov esi, 42 mov edi, OFFSET FLAT:.LC0 xor eax, eax call printf xor eax, eax add rsp, 8 ret
神箭
不 printf
{fmt} 提供完整的運行時類型安全性。體損壞和潛在的崩潰。
早在2020年,我就投入了一些時間來優化庫大小,成功將其減少到100kB以下(僅約57kB) -Os -flto
)。二進位的大小,並看看是否可以進一步減少。
但有人說,為什麼是二進位大小呢?
人們對在記憶體建立裝置上使用 {fmt} 非常感興趣,請參閱 #758 和 #1226 中的兩個遙遠過去的範例。上使用{fmt}。
我們將應用與先前相同的工作方法,檢查使用 {fmt} 的程式的執行檔大小,因為這與最終使用者最相關。
首先,讓我們建立基準:最新版本的{fmt} (11.0.2) 的二進位大小是多少?
$ git checkout 11.0.2$ g++ -Os -flto -DNDEBUG -I include test.cc src/format.cc$ strip a.out && ls -lh a.out-rwxrwxr-x 1 vagrant vagrant 75K Aug 30 19:24 a.out
產生的第二進大小為75kB(已分割)。
現在,讓我們探索潛在的優化。值的傳統),但仍可跨越 L
格式說明符。FMT_STATIC_THOUSANDS_SEPARATOR
宏:
$ g++ -Os -flto -DNDEBUG "-DFMT_STATIC_THOUSANDS_SEPARATOR=','" -I include test.cc src/format.cc$ strip a.out && ls -lh a.out-rwxrwxr-x 1 vagrant vagrant 71K Aug 30 19:25 a.out
中斷區域設定支援將二進位大小減少到 71kB。
接下來,讓我們使用我們值得信賴的工具 Bloaty 檢查結果:
$ bloaty -d symbols a.out FILE SIZE VM SIZE -------------- -------------- 43.8% 41.1Ki 43.6% 29.0Ki [121 Others] 6.4% 6.04Ki 8.1% 5.42Ki fmt::v11::detail::do_write_float() 5.9% 5.50Ki 7.5% 4.98Ki fmt::v11::detail::write_int_noinline() 5.7% 5.32Ki 5.8% 3.88Ki fmt::v11::detail::write() 5.4% 5.02Ki 7.2% 4.81Ki fmt::v11::detail::parse_replacement_field() 3.9% 3.69Ki 3.7% 2.49Ki fmt::v11::detail::format_uint() 3.2% 3.00Ki 0.0% 0 [section .symtab] 2.7% 2.50Ki 0.0% 0 [section .strtab] 2.3% 2.12Ki 2.9% 1.93Ki fmt::v11::detail::dragonbox::to_decimal() 2.0% 1.89Ki 2.4% 1.61Ki fmt::v11::detail::write_int() 2.0% 1.88Ki 0.0% 0 [ELF Section Headers] 1.9% 1.79Ki 2.5% 1.66Ki fmt::v11::detail::write_float() 1.9% 1.78Ki 2.7% 1.78Ki [section .dynstr] 1.8% 1.72Ki 2.4% 1.62Ki fmt::v11::detail::format_dragon() 1.8% 1.68Ki 1.5% 1016 fmt::v11::detail::format_decimal() 1.6% 1.52Ki 2.1% 1.41Ki fmt::v11::detail::format_float() 1.6% 1.49Ki 0.0% 0 [Unmapped] 1.5% 1.45Ki 2.2% 1.45Ki [section .dynsym] 1.5% 1.45Ki 2.0% 1.31Ki fmt::v11::detail::write_loc() 1.5% 1.44Ki 2.2% 1.44Ki [section .rodata] 1.5% 1.40Ki 1.1% 764 fmt::v11::detail::do_write_float()::{lambda()#2}::operator()() 100.0% 93.8Ki 100.0% 66.6Ki TOTAL
毫無疑問,二進位大小的很大一部分專用於數字格式,特別是浮點。它的方法,儘管該方法有些特殊且無法分割其他類型。
核心問題是格式化函數需要了解所有可格式化類型。 printf
由 C 標準定義,但不一定適用於 {fmt}。於二進位大小可能需要不同的方法。
我對這個想法做了一個實驗性的實現。FMT_BUILTIN_TYPES
宏設定為0,僅 int
進行特殊處理,所有其他類型都使用通用增強API。int
例如,對於動態寬度和精度
fmt::print("{:{}}n", "hello", 10); // prints "hello "
這為您提供了「不用為不使用付費的東西」的模型,儘管它會稍微增加每次呼叫的二進位大小。 如果您確實添加浮點數或其他類型,相關程式碼仍將包含在建置中置中。
和 FMT_BUILTIN_TYPES=0
,我們範例中的二進位大小減少到 31kB,代表顯著的改進:
$ git checkout 377cf20$ g++ -Os -flto -DNDEBUG "-DFMT_STATIC_THOUSANDS_SEPARATOR=','" -DFMT_BUILTIN_TYPES=0 -I include test.cc src/format.cc$ strip a.out && ls -lh a.out-rwxrwxr-x 1 vagrant vagrant 31K Aug 30 19:37 a.out
然而,更新後的 Bloaty 結果揭示了一些揮之不去的語言環境痕跡,例如 digit_grouping
:
$ bloaty -d fullsymbols a.out FILE SIZE VM SIZE -------------- -------------- 41.8% 18.0Ki 39.7% 11.0Ki [84 Others] 6.4% 2.77Ki 0.0% 0 [section .symtab] 5.3% 2.28Ki 0.0% 0 [section .strtab] 4.6% 1.99Ki 6.9% 1.90Ki fmt::v11::detail::format_handler::on_format_specs(int, char const*, char const*) 4.4% 1.88Ki 0.0% 0 [ELF Section Headers] 4.1% 1.78Ki 5.8% 1.61Ki fmt::v11::basic_appender fmt::v11::detail::write_int_noinline, unsigned int>(fmt::v11::basic_appender, fmt::v11::detail::write_int_arg, fmt::v11::format_specs const&, fmt::v11::detail::locale_ref) (.constprop.0) 3.7% 1.60Ki 5.8% 1.60Ki [section .dynstr] 3.5% 1.50Ki 4.8% 1.34Ki void fmt::v11::detail::vformat_to(fmt::v11::detail::buffer&, fmt::v11::basic_string_view, fmt::v11::detail::vformat_args::type, fmt::v11::detail::locale_ref) (.constprop.0) 3.5% 1.49Ki 4.9% 1.35Ki fmt::v11::basic_appender fmt::v11::detail::write_int<:v11::basic_appender>, unsigned __int128, char>(fmt::v11::basic_appender, unsigned __int128, unsigned int, fmt::v11::format_specs const&, fmt::v11::detail::digit_grouping const&) 3.1% 1.31Ki 4.7% 1.31Ki [section .dynsym] 3.0% 1.29Ki 4.2% 1.15Ki fmt::v11::basic_appender fmt::v11::detail::write_int<:v11::basic_appender>, unsigned long, char>(fmt::v11::basic_appender, unsigned long, unsigned int, fmt::v11::format_specs const&, fmt::v11::detail::digit_grouping const&)
在提交 e582d37 和 b3ccc2d 中破壞了這些工件,並引入了一個更友善的使用者選項來穿透 FMT_USE_LOCALE
宏,二進位大小最小 27kB:
$ git checkout b3ccc2d$ g++ -Os -flto -DNDEBUG -DFMT_USE_LOCALE=0 -DFMT_BUILTIN_TYPES=0 -I include test.cc src/format.cc$ strip a.out && ls -lh a.out-rwxrwxr-x 1 vagrant vagrant 27K Aug 30 19:38 a.out
該庫包括幾個區域,這些區域以大小換取速度。
auto do_count_digits(uint32_t n) -> int {// An optimization by Kendall Willets from https://bit.ly/3uOIQrB.// This increments the upper 32 bits (log10(T) - 1) when>=T is added.# define FMT_INC(T) (((sizeof(#T) - 1ull) static constexpr uint64_t table[] = { FMT_INC(0), FMT_INC(0), FMT_INC(0), // 8 FMT_INC(10), FMT_INC(10), FMT_INC(10), // 64 FMT_INC(100), FMT_INC(100), FMT_INC(100), // 512 FMT_INC(1000), FMT_INC(1000), FMT_INC(1000), // 4096 FMT_INC(10000), FMT_INC(10000), FMT_INC(10000), // 32k FMT_INC(100000), FMT_INC(100000), FMT_INC(100000), // 256k FMT_INC(1000000), FMT_INC(1000000), FMT_INC(1000000), // 2048k FMT_INC(10000000), FMT_INC(10000000), FMT_INC(10000000), // 16M FMT_INC(100000000), FMT_INC(100000000), FMT_INC(100000000), // 128M FMT_INC(1000000000), FMT_INC(1000000000), FMT_INC(1000000000), // 1024M FMT_INC(1000000000), FMT_INC(1000000000) // 4B }; auto inc = table[__builtin_clz(n | 1) ^ 31]; return static_castint>((n + inc) >> 32);}
這裡使用的表是256位元組。 __builtin_clz
不可用,例如 constexpr
:
template typename T> constexpr auto count_digits_fallback(T n) -> int { int count = 1; for (;;) { // Integer division is slow so do it for a group of four digits instead // of for every digit. The idea comes from the talk by Alexandrescu // "Three Optimization Tips for C++". See speed-test for a comparison. if (n 10) return count; if (n 100) return count + 1; if (n 1000) return count + 2; if (n 10000) return count + 3; n /= 10000u; count += 4; }}
剩下的就是提供使用者何時跨越(你猜對了)另一個配置宏使用回退實現的控制,FMT_OPTIMIZE_SIZE
:
auto count_digits(uint32_t n) -> int {#ifdef FMT_BUILTIN_CLZ if (!is_constant_evaluated() && !FMT_OPTIMIZE_SIZE) return do_count_digits(n);#endif return count_digits_fallback(n);}
透過這個調整和一些類似的調整,我們將二進位大小減少到 23kB:
$ git checkout 8e3da9d$ g++ -Os -flto -DNDEBUG -I include -DFMT_USE_LOCALE=0 -DFMT_BUILTIN_TYPES=0 -DFMT_OPTIMIZE_SIZE=1 test.cc src/format.cc$ strip a.out && ls -lh a.out-rwxrwxr-x 1 vagrant vagrant 23K Aug 30 19:41 a.out
我們可以透過額外的調整進一步減少二進位檔案的大小,但讓我們解決房間裡的象,當然,C++ 標準函式庫。什麼意義呢?
雖然{fmt}對標準函式庫的依賴程度最低,但是否可以將其依賴項作為完全刪除? FMT_THROW
,例如將其定義為 abort
。
讓我們嘗試一下並編譯 -nodefaultlibs
並取消異常:
$ g++ -Os -flto -DNDEBUG -I include -DFMT_USE_LOCALE=0 -DFMT_BUILTIN_TYPES=0 -DFMT_OPTIMIZE_SIZE=1 '-DFMT_THROW(s)=abort()' -fno-exceptions test.cc src/format.cc -nodefaultlibs -lc/usr/bin/ld: /tmp/cc04DFeK.ltrans0.ltrans.o: in function `fmt::v11::basic_memory_buffer>::grow(fmt::v11::detail::buffer&, unsigned long)'::(.text+0xaa8): undefined reference to `std::__throw_bad_alloc()'/usr/bin/ld: :(.text+0xab8): undefined reference to `operator new(unsigned long)'/usr/bin/ld: :(.text+0xaf8): undefined reference to `operator delete(void*, unsigned long)'/usr/bin/ld: /tmp/cc04DFeK.ltrans0.ltrans.o: in function `fmt::v11::vprint_buffered(_IO_FILE*, fmt::v11::basic_string_view, fmt::v11::basic_format_args<:v11::context>) [clone .constprop.0]'::(.text+0x18c4): undefined reference to `operator delete(void*, unsigned long)'collect2: error: ld returned 1 exit status
令人驚訝的是,這種方法最有效。 fmt::basic_memory_buffer
,這是一個很小的分佈密度,可以在必要時生長成動態記憶體。
fmt::print
可以直接寫入 FILE
達拉斯,一般需要動態分配。fmt::basic_memory_buffer
從 fmt::print
然而,由於它可能在其他地方使用,更好的解決方案將預設分配器替換為使用的分配器 malloc
和 free
而不是 new
和 delete
。
template typename T> struct allocator { using value_type = T; T* allocate(size_t n) { FMT_ASSERT(n max_valuesize_t>() / sizeof(T), ""); T* p = static_castT*>(malloc(n * sizeof(T))); if (!p) FMT_THROW(std::bad_alloc()); return p; } void deallocate(T* p, size_t) { free(p); }};
這將二進位大小減少到僅 14kB:
$ git checkout c0fab5e$ g++ -Os -flto -DNDEBUG -I include -DFMT_USE_LOCALE=0 -DFMT_BUILTIN_TYPES=0 -DFMT_OPTIMIZE_SIZE=1 '-DFMT_THROW(s)=abort()' -fno-exceptions test.cc src/format.cc -nodefaultlibs -lc$ strip a.out && ls -lh a.out-rwxrwxr-x 1 vagrant vagrant 14K Aug 30 19:06 a.out
考慮一個標記的 C 程序 main
這裡的函數系統上為6kB,{fmt}現在在二進位檔案中新增了不到10kB。
我們也可以輕鬆驗證它不再依賴C++執行階段:
$ ldd a.out linux-vdso.so.1 (0x0000ffffb0738000) libc.so.6=> /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffffb0530000) /lib/ld-linux-aarch64.so.1 (0x0000ffffb06ff000)
希望您找到這個有趣又快樂的嵌入格式!
最後修改於2024-08-30