std::string_view 函數傳值與編譯器最佳化

Jun 28, 2022·

2 min read

std::string_view 函數傳值與編譯器最佳化

在使用 c++17std::string_view 時肯定會有這個疑問:

我們該用 const string_view& 還是 string_view

因為他本身就是 reference 的意思了,所以在函數傳值時,直接傳值比較符合語意。

可是考慮到效能:他又至少是一個 struct 包含一個指標和整數,是不是該傳 const&?但這就像傳一個 reference 的 reference?

這裡從編譯器最佳化給出一個看法:傳值即可

主要是用 gcc x86-64 -std=c++17 -O3 的結果。 MSVC 可以考慮使用 /O2 ,也會有類似的結果。

實驗

這個實驗抽自Three reasons to pass std::string_view by value,然後再加一個 struct 的測試。

我們可以做個實驗 (godbolt):

#include <string_view>

void byvalue(std::string_view sv);
void byref(const std::string_view& sv);

void callbyvalue() {
    byvalue("hello");
}

void callbyref() {
    byref("hello");
}
.LC0:
  .string "hello"

callbyvalue():
  mov edi, 5
  mov esi, OFFSET FLAT:.LC0
  jmp byvalue(std::basic_string_view<char, std::char_traits<char> >)

callbyref():
  sub rsp, 24
  mov rdi, rsp
  mov QWORD PTR [rsp], 5
  mov QWORD PTR [rsp+8], OFFSET FLAT:.LC0
  call byref(std::basic_string_view<char, std::char_traits<char> > const&)
  add rsp, 24
  ret

兩個差異在 call by value 實際上並沒有真的建立一個 std::string_view instance。

他就只是像直接傳兩個參數到函數裡面: const char pointer 和 int 。

但是 call by reference 卻會要先建立一個東西再傳過去。

但上面的例子我們會有疑慮:這是不是只是因為傳 const char pointer 才會這樣?,

我們可以再用一個 struct 來實驗 (godbolt):

struct Foo {
    int a;
    const char* p;
};

void bar(int, const char*);

void byvalue(Foo sv);
void byref(const Foo& sv);

void cbar() {
    bar(5, "hello");
}

void cbval() {
    byvalue({5, "hello"});
}

void cbref() {
    byref({5, "hello"});
}

結果

.LC0:
  .string "hello"

cbar():
  mov esi, OFFSET FLAT:.LC0
  mov edi, 5
  jmp bar(int, char const*)

cbval():
  mov edi, 5
  mov esi, OFFSET FLAT:.LC0
  jmp byvalue(Foo)

cbref():
  sub rsp, 24
  mov rdi, rsp
  mov DWORD PTR [rsp], 5
  mov QWORD PTR [rsp+8], OFFSET FLAT:.LC0
  call byref(Foo const&)
  add rsp, 24
  ret

可以發現,cbarcbval 基本上是一樣的。他們的意思都是傳兩個參數。 cbref 就是建立一個 struct 然後傳進去。

-O3 的情況下,cbref 竟然意外的比 cbval 多操作!

結論

既然編譯器讓這兩個選擇的效能差異並沒有特別的突出(甚至有時會更好),那語意的選擇便更為重要:傳值就好

參考資料