簡體   English   中英

`std::variant` vs. 繼承 vs. 其他方式(性能)

[英]`std::variant` vs. inheritance vs. other ways (performance)

我想知道std::variant的性能。 我什么時候不應該使用它? 似乎虛函數仍然比使用std::visit好得多,這讓我感到驚訝!

在“A Tour of C++”中,Bjarne Stroustrup 在解釋了std::holds_alternativesoverloaded方法之后談到了pattern checking

這基本上相當於一個虛函數調用,但可能更快。 與所有性能聲明一樣,當性能至關重要時,應該通過測量來驗證這種“可能更快”。 對於大多數用途,性能差異是微不足道的。

我已經對我想到的一些方法進行了基准測試,結果如下:

基准 http://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg

如果你開啟優化,你會得到不同的結果:

啟用 op 的基准測試

http://quick-bench.com/p6KIUtRxZdHJeiFiGI8gjbOumoc

這是我用於基准測試的代碼; 我確信有更好的方法來實現和使用變體來使用它們而不是虛擬關鍵字( 繼承與 std::variant ):

刪除舊代碼; 看看更新

誰能解釋為std::variant實現這個用例的最佳方法是什么,讓我進行測試和基准測試:

我目前正在實現RFC 3986 ,它是“URI”,對於我的用例,這個類將更多地用作 const,可能不會有太大變化,用戶更有可能使用這個類來查找每個特定的URI 的一部分,而不是制作一個 URI; 因此使用std::string_view而不將 URI 的每個段分隔在其自己的std::string中是有意義的。 問題是我需要為它實現兩個類; 當我只需要一個 const 版本時; 另一個用於當用戶想要創建 URI 而不是提供一個並通過它進行搜索時。

所以我用了一個template來修復它自己的問題; 但后來我意識到我可以使用std::variant<std::string, std::string_view> (或者可能std::variant<CustomStructHoldingAllThePieces, std::string_view> ); 所以我開始研究看看它是否真的有助於使用變體。 從這些結果來看,如果我不想實現兩個不同const_uriuri類,似乎使用繼承和virtual是我最好的選擇。

你覺得我應該怎么做?


更新 (2)

感謝@gan_ 在我的基准代碼中提到並修復了提升問題。 基准 http://quick-bench.com/Mcclomh03nu8nDCgT3T302xKnXY

我對 try-catch hell 的結果感到驚訝,但感謝這個現在有意義的評論

更新 (3)

我刪除了try-catch方法,因為它真的很糟糕; 並且還隨機更改了選定的值,從外觀上看,我看到了更現實的基准。 看來virtual畢竟不是正確的答案。 隨機訪問 http://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt0

http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs (沒有內存泄漏哈哈)

更新 (4)

我消除了生成隨機數的開銷(我在上次更新中已經這樣做了,但似乎我抓住了錯誤的 URL 進行基准測試)並添加了一個 EmptyRandom 來了解生成隨機數的基線。 並且還在 Virtual 中做了一些小的改動,但我認為它不會影響任何東西。 空隨機添加 http://quick-bench.com/EmhM-S-xoA0LABYK6yrMyBb8UeI

http://quick-bench.com/5hBZprSRIRGuDaBZ_wj0cOwnNhw (刪除了虛擬,以便您可以更好地比較其余部分)


更新 (5)

正如 Jorge Bellon 在評論中所說,我沒有考慮分配成本; 所以我將每個基准轉換為使用指針。 這種間接性當然會對性能產生影響,但現在更公平了。 所以現在循環中沒有分配。

這是代碼:

刪除舊代碼; 看看更新

到目前為止,我運行了一些基准測試。 似乎 g++ 在優化代碼方面做得更好:

-------------------------------------------------------------------
Benchmark                         Time             CPU   Iterations
-------------------------------------------------------------------
EmptyRandom                   0.756 ns        0.748 ns    746067433
TradeSpaceForPerformance       2.87 ns         2.86 ns    243756914
Virtual                        12.5 ns         12.4 ns     60757698
Index                          7.85 ns         7.81 ns     99243512
GetIf                          8.20 ns         8.18 ns     92393200
HoldsAlternative               7.08 ns         7.07 ns     96959764
ConstexprVisitor               11.3 ns         11.2 ns     60152725
StructVisitor                  10.7 ns         10.6 ns     60254088
Overload                       10.3 ns         10.3 ns     58591608

對於鏗鏘聲:

-------------------------------------------------------------------
Benchmark                         Time             CPU   Iterations
-------------------------------------------------------------------
EmptyRandom                    1.99 ns         1.99 ns    310094223
TradeSpaceForPerformance       8.82 ns         8.79 ns     87695977
Virtual                        12.9 ns         12.8 ns     51913962
Index                          13.9 ns         13.8 ns     52987698
GetIf                          15.1 ns         15.0 ns     48578587
HoldsAlternative               13.1 ns         13.1 ns     51711783
ConstexprVisitor               13.8 ns         13.8 ns     49120024
StructVisitor                  14.5 ns         14.5 ns     52679532
Overload                       17.1 ns         17.1 ns     42553366

現在,對於clang,最好使用虛擬繼承,但對於g++,最好使用holds_alternativeget_if ,但總的來說,到目前為止, std::visit對於我的幾乎所有基准測試似乎都不是一個好的選擇。

我認為如果將模式匹配(能夠檢查除整數文字之外的更多內容的 switch 語句)添加到 c++ 中將是一個好主意,我們將編寫更清潔和更可維護的代碼。

我想知道package.index()結果。 不應該更快嗎? 它有什么作用?

Clang 版本:http: //quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI

根據Maxim Egorushkin 的評論,使用One one而不是auto one = new One的版本:http: //quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q (對結果的影響不大)


更新 (6)

我做了一些更改,結果現在從編譯器到編譯器有很大不同。 但似乎std::get_ifstd::holds_alternatives是最好的解決方案。 由於未知原因, virtual現在似乎與 clang 一起工作得最好。 這真的讓我感到驚訝,因為我記得virtual在 gcc 中表現更好。 而且std::visit完全沒有競爭; 在最后一個基准測試中,它甚至比 vtable 查找還要糟糕。

這是基准(使用 GCC/Clang 以及 libstdc++ 和 libc++ 運行它):

http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y

#include <benchmark/benchmark.h>

#include <array>
#include <variant>
#include <random>
#include <functional>
#include <algorithm>

using namespace std;

struct One {
  auto get () const { return 1; }
 };
struct Two {
  auto get() const { return 2; }
 };
struct Three { 
  auto get() const { return 3; }
};
struct Four {
  auto get() const { return 4; }
 };

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;


std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<std::mt19937::result_type> random_pick(0,3); // distribution in range [1, 6]

template <std::size_t N>
std::array<int, N> get_random_array() {
  std::array<int, N> item;
  for (int i = 0 ; i < N; i++)
    item[i] = random_pick(rng);
  return item;
}

template <typename T, std::size_t N>
std::array<T, N> get_random_objects(std::function<T(decltype(random_pick(rng)))> func) {
    std::array<T, N> a;
    std::generate(a.begin(), a.end(), [&] {
        return func(random_pick(rng));
    });
    return a;
}


static void TradeSpaceForPerformance(benchmark::State& state) {
    One one;
    Two two;
    Three three;
    Four four;

  int index = 0;

  auto ran_arr = get_random_array<50>();
  int r = 0;

  auto pick_randomly = [&] () {
    index = ran_arr[r++ % ran_arr.size()];
  };

  pick_randomly();


  for (auto _ : state) {

    int res;
    switch (index) {
      case 0:
        res = one.get();
        break;
      case 1:
        res = two.get();
        break;
      case 2:
        res = three.get();
        break;
      case 3:
        res = four.get();
        break;
    }
    
    benchmark::DoNotOptimize(index);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }


}
// Register the function as a benchmark
BENCHMARK(TradeSpaceForPerformance);


static void Virtual(benchmark::State& state) {

  struct Base {
    virtual int get() const noexcept = 0;
    virtual ~Base() {}
  };

  struct A final: public Base {
    int get()  const noexcept override { return 1; }
  };

  struct B final : public Base {
    int get() const noexcept override { return 2; }
  };

  struct C final : public Base {
    int get() const noexcept override { return 3; }
  };

  struct D final : public Base {
    int get() const noexcept override { return 4; }
  };

  Base* package = nullptr;
  int r = 0;
  auto packages = get_random_objects<Base*, 50>([&] (auto r) -> Base* {
          switch(r) {
              case 0: return new A;
              case 1: return new B;
              case 3: return new C;
              case 4: return new D;
              default: return new C;
          }
    });

  auto pick_randomly = [&] () {
    package = packages[r++ % packages.size()];
  };

  pick_randomly();

  for (auto _ : state) {

    int res = package->get();

    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }


  for (auto &i : packages)
    delete i;

}
BENCHMARK(Virtual);




static void FunctionPointerList(benchmark::State& state) {

    One one;
    Two two;
    Three three;
    Four four;
  using type = std::function<int()>;
  std::size_t index;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
        case 0: return std::bind(&One::get, one);
        case 1: return std::bind(&Two::get, two);
        case 2: return std::bind(&Three::get, three);
        case 3: return std::bind(&Four::get, four);
        default: return std::bind(&Three::get, three);
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    index = r++ % packages.size();
  };


  pick_randomly();

  for (auto _ : state) {

    int res = packages[index]();

    benchmark::DoNotOptimize(index);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }

}
BENCHMARK(FunctionPointerList);



static void Index(benchmark::State& state) {

    One one;
    Two two;
    Three three;
    Four four;
  using type = std::variant<One, Two, Three, Four>;
  type* package = nullptr;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
            case 0: return one;
            case 1: return two;
            case 2: return three;
            case 3: return four;
            default: return three;
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    package = &packages[r++ % packages.size()];
  };


  pick_randomly();

  for (auto _ : state) {

    int res;
    switch (package->index()) {
      case 0: 
        res = std::get<One>(*package).get();
        break;
      case 1:
        res = std::get<Two>(*package).get();
        break;
      case 2:
        res = std::get<Three>(*package).get();
        break;
      case 3:
        res = std::get<Four>(*package).get();
        break;
    }

    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }

}
BENCHMARK(Index);



static void GetIf(benchmark::State& state) {
    One one;
    Two two;
    Three three;
    Four four;
  using type = std::variant<One, Two, Three, Four>;
  type* package = nullptr;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
            case 0: return one;
            case 1: return two;
            case 2: return three;
            case 3: return four;
            default: return three;
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    package = &packages[r++ % packages.size()];
  };

  pick_randomly();

  for (auto _ : state) {

    int res;
    if (auto item = std::get_if<One>(package)) {
      res = item->get();
    } else if (auto item = std::get_if<Two>(package)) {
      res = item->get();
    } else if (auto item = std::get_if<Three>(package)) {
      res = item->get();
    } else if (auto item = std::get_if<Four>(package)) {
      res = item->get();
    }

    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }
  

}
BENCHMARK(GetIf);

static void HoldsAlternative(benchmark::State& state) {
    One one;
    Two two;
    Three three;
    Four four;
  using type = std::variant<One, Two, Three, Four>;
  type* package = nullptr;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
            case 0: return one;
            case 1: return two;
            case 2: return three;
            case 3: return four;
            default: return three;
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    package = &packages[r++ % packages.size()];
  };

  pick_randomly();

  for (auto _ : state) {

    int res;
    if (std::holds_alternative<One>(*package)) {
      res = std::get<One>(*package).get();
    } else if (std::holds_alternative<Two>(*package)) {
      res = std::get<Two>(*package).get();
    } else if (std::holds_alternative<Three>(*package)) {
      res = std::get<Three>(*package).get();
    } else if (std::holds_alternative<Four>(*package)) {
      res = std::get<Four>(*package).get();
    }

    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }

}
BENCHMARK(HoldsAlternative);


static void ConstexprVisitor(benchmark::State& state) {

    One one;
    Two two;
    Three three;
    Four four;
  using type = std::variant<One, Two, Three, Four>;
  type* package = nullptr;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
            case 0: return one;
            case 1: return two;
            case 2: return three;
            case 3: return four;
            default: return three;
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    package = &packages[r++ % packages.size()];
  };

  pick_randomly();

  auto func = [] (auto const& ref) {
        using type = std::decay_t<decltype(ref)>;
        if constexpr (std::is_same<type, One>::value) {
            return ref.get();
        } else if constexpr (std::is_same<type, Two>::value) {
            return ref.get();
        } else if constexpr (std::is_same<type, Three>::value)  {
          return ref.get();
        } else if constexpr (std::is_same<type, Four>::value) {
            return ref.get();
        } else {
          return 0;
        }
    };

  for (auto _ : state) {

    auto res = std::visit(func, *package);

    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }

}
BENCHMARK(ConstexprVisitor);

static void StructVisitor(benchmark::State& state) {

  

  struct VisitPackage
  {
      auto operator()(One const& r) { return r.get(); }
      auto operator()(Two const& r) { return r.get(); }
      auto operator()(Three const& r) { return r.get(); }
      auto operator()(Four const& r) { return r.get(); }
  };

    One one;
    Two two;
    Three three;
    Four four;
  using type = std::variant<One, Two, Three, Four>;
  type* package = nullptr;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
            case 0: return one;
            case 1: return two;
            case 2: return three;
            case 3: return four;
            default: return three;
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    package = &packages[r++ % packages.size()];
  };

  pick_randomly();

  auto vs = VisitPackage();

  for (auto _ : state) {

    auto res = std::visit(vs, *package);

    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }

}
BENCHMARK(StructVisitor);


static void Overload(benchmark::State& state) {


    One one;
    Two two;
    Three three;
    Four four;
  using type = std::variant<One, Two, Three, Four>;
  type* package = nullptr;

  auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
        switch(r) {
            case 0: return one;
            case 1: return two;
            case 2: return three;
            case 3: return four;
            default: return three;
        }
    });
  int r = 0;

  auto pick_randomly = [&] () {
    package = &packages[r++ % packages.size()];
  };

  pick_randomly();

  auto ov = overload {
      [] (One const& r) { return r.get(); },
      [] (Two const& r) { return r.get(); },
      [] (Three const& r) { return r.get(); },
      [] (Four const& r) { return r.get(); }
    };

  for (auto _ : state) {

    auto res = std::visit(ov, *package);

  
    benchmark::DoNotOptimize(package);
    benchmark::DoNotOptimize(res);

    pick_randomly();
  }

}
BENCHMARK(Overload);


// BENCHMARK_MAIN();

GCC 編譯器的結果:

-------------------------------------------------------------------
Benchmark                         Time             CPU   Iterations
-------------------------------------------------------------------
TradeSpaceForPerformance       3.71 ns         3.61 ns    170515835
Virtual                       12.20 ns        12.10 ns     55911685
FunctionPointerList           13.00 ns        12.90 ns     50763964
Index                          7.40 ns         7.38 ns    136228156
GetIf                          4.04 ns         4.02 ns    205214632
HoldsAlternative               3.74 ns         3.73 ns    200278724
ConstexprVisitor              12.50 ns        12.40 ns     56373704
StructVisitor                 12.00 ns        12.00 ns     60866510
Overload                      13.20 ns        13.20 ns     56128558

clang 編譯器的結果(我對此感到驚訝):

-------------------------------------------------------------------
Benchmark                         Time             CPU   Iterations
-------------------------------------------------------------------
TradeSpaceForPerformance       8.07 ns         7.99 ns     77530258
Virtual                        7.80 ns         7.77 ns     77301370
FunctionPointerList            12.1 ns         12.1 ns     56363372
Index                          11.1 ns         11.1 ns     69582297
GetIf                          10.4 ns         10.4 ns     80923874
HoldsAlternative               9.98 ns         9.96 ns     71313572
ConstexprVisitor               11.4 ns         11.3 ns     63267967
StructVisitor                  10.8 ns         10.7 ns     65477522
Overload                       11.4 ns         11.4 ns     64880956

迄今為止最好的基准(將被更新):http: //quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y (也可以查看 GCC)

std::visit在某些實現上似乎還缺乏一些優化。 話雖這么說,在這個類似實驗室的設置中,有一個中心點不太明顯——即基於的設計是基於堆棧的,而虛擬模式自然會傾向於基於堆。 在現實世界的場景中,這意味着內存布局很可能是碎片化的(可能隨着時間的推移——一旦對象離開緩存等)——除非它可以以某種方式避免。 相反的是可以在連續內存中布局的基於的設計。 我相信這是一個非常重要的考慮點,當涉及到不可低估的性能時。

為了說明這一點,請考慮以下內容:

std::vector<Base*> runtime_poly_;//risk of fragmentation

對比

std::vector<my_var_type> cp_time_poly_;//no fragmentation (but padding 'risk')

這種碎片化很難構建到像這樣的基准測試中。 如果這(也)在 bjarne 聲明的上下文中,當他說它可能會更快(我相信這是正確的)時,我不清楚。

對於基於std::variant的設計,要記住的另一件非常重要的事情是每個元素的大小用盡了最大可能元素的大小。 因此,如果對象沒有大致相同的大小,則必須仔細考慮,因為它可能會對緩存產生不良影響。

綜合考慮這些點,很難說在一般情況下哪個最好使用 - 但是如果該集合是一個大致相同大小的封閉“小型”集合,則應該足夠清楚 - 然后變體樣式顯示出更快的巨大潛力(如 bjarne 筆記)。

我們現在只考慮性能,選擇其中一種模式確實還有其他原因:最后,您只需要走出舒適的“實驗室”,設計和基准測試您的真實世界用例。

如果您可以保證變體永遠不會因異常為空,則可以將它們全部與訪問實現匹配。 這是一個單一的訪問者,它與上面的虛擬匹配並且與 jmp 表很好地內聯。 https://gcc.godbolt.org/z/kkjACx

struct overload : Fs... {
  using Fs::operator()...;
};

template <typename... Fs>
overload(Fs...) -> overload<Fs...>;

template <size_t N, typename R, typename Variant, typename Visitor>
[[nodiscard]] constexpr R visit_nt(Variant &&var, Visitor &&vis) {
  if constexpr (N == 0) {
    if (N == var.index()) {
      // If this check isnt there the compiler will generate
      // exception code, this stops that
      return std::forward<Visitor>(vis)(
          std::get<N>(std::forward<Variant>(var)));
    }
  } else {
    if (var.index() == N) {
      return std::forward<Visitor>(vis)(
          std::get<N>(std::forward<Variant>(var)));
    }
    return visit_nt<N - 1, R>(std::forward<Variant>(var),
                              std::forward<Visitor>(vis));
  }
  while (true) {
  }  // unreachable but compilers complain
}

template <class... Args, typename Visitor, typename... Visitors>
[[nodiscard]] constexpr decltype(auto) visit_nt(
    std::variant<Args...> const &var, Visitor &&vis, Visitors &&... visitors) {
  auto ol =
      overload{std::forward<Visitor>(vis), std::forward<Visitors>(visitors)...};
  using result_t = decltype(std::invoke(std::move(ol), std::get<0>(var)));

  static_assert(sizeof...(Args) > 0);
  return visit_nt<sizeof...(Args) - 1, result_t>(var, std::move(ol));
}

template <class... Args, typename Visitor, typename... Visitors>
[[nodiscard]] constexpr decltype(auto) visit_nt(std::variant<Args...> &var,
                                                Visitor &&vis,
                                                Visitors &&... visitors) {
  auto ol =
      overload(std::forward<Visitor>(vis), std::forward<Visitors>(visitors)...);
  using result_t = decltype(std::invoke(std::move(ol), std::get<0>(var)));

  static_assert(sizeof...(Args) > 0);
  return visit_nt<sizeof...(Args) - 1, result_t>(var, std::move(ol));
}

template <class... Args, typename Visitor, typename... Visitors>
[[nodiscard]] constexpr decltype(auto) visit_nt(std::variant<Args...> &&var,
                                                Visitor &&vis,
                                                Visitors &&... visitors) {
  auto ol =
      overload{std::forward<Visitor>(vis), std::forward<Visitors>(visitors)...};
  using result_t =
      decltype(std::invoke(std::move(ol), std::move(std::get<0>(var))));

  static_assert(sizeof...(Args) > 0);
  return visit_nt<sizeof...(Args) - 1, result_t>(std::move(var), std::move(ol));
}

template <typename Value, typename... Visitors>
inline constexpr bool is_visitable_v = (std::is_invocable_v<Visitors, Value> or
                                        ...);

您首先使用變體調用它,然后是訪問者。 這是已添加的 Update 6 quickbench 顯示 visit_nt 性能的 Quickbench 基准測試 . 長凳的鏈接在這里http://quick-bench.com/98aSbU0wWUsym0ej-jLy1POmCBw

因此,我認為是否訪問的決定歸結為更具表現力和意圖的明確性。 無論哪種方式都可以實現性能。

基於更新 6 http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y

我想我們無法比較時間,但相對於彼此的結果似乎不同,足以顯示庫實現中的選擇。

  • 視覺2019 v16.8.3

  • cl 19.28.29335 x64

  • 在 /std:c++17 中編譯

     Run on (8 X 3411 MHz CPU s) CPU Caches: L1 Data 32 KiB (x4) L1 Instruction 32 KiB (x4) L2 Unified 256 KiB (x4) L3 Unified 8192 KiB (x1) ------------------------------------------------------------------- Benchmark Time CPU Iterations ------------------------------------------------------------------- TradeSpaceForPerformance 5.41 ns 5.47 ns 100000000 Virtual 11.2 ns 10.9 ns 56000000 FunctionPointerList 13.2 ns 13.1 ns 56000000 Index 4.37 ns 4.37 ns 139377778 GetIf 4.79 ns 4.87 ns 144516129 HoldsAlternative 5.08 ns 5.16 ns 100000000 ConstexprVisitor 4.16 ns 4.14 ns 165925926 StructVisitor 4.26 ns 4.24 ns 165925926 Overload 4.21 ns 4.24 ns 165925926

我在這里添加了AutoVisitConstVisithttps ://quick-bench.com/q/qKvbnsqH1MILeQNWg3XpFfS9f3s

    auto res = std::visit([](auto && v) { return v.get(); }, *package);

這是迄今為止最短的解決方案。

並且還將所有隨機初始化內容移動到一個宏中,以提高各種實現的可讀性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM