在C ++中执行语句顺序


111

假设我有许多要按固定顺序执行的语句。我想在优化级别2中使用g ++,因此某些语句可以重新排序。一个人必须使用哪些工具来强制执行某种语句排序?

考虑以下示例。

using Clock = std::chrono::high_resolution_clock;

auto t1 = Clock::now(); // Statement 1
foo();                  // Statement 2
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

在此示例中,以给定顺序执行语句1-3非常重要。但是,编译器是否不能认为语句2独立于1和3并按以下方式执行代码?

using Clock=std::chrono::high_resolution_clock;

foo();                  // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

34
如果编译器认为它们在不同时是独立的,则说明该编译器已损坏,您应该使用更好的编译器。
David Schwartz


1
可能__sync_synchronize()有什么帮助吗?
vsz

3
@HowardHinnant:如果定义了这样的指令,并且如果调整了别名规则以免除在其之前写入的数据障碍之后执行的读取操作,则标准C的语义功能将得到极大的改善。
超级猫

4
@DavidSchwartz在这种情况下,它是关于foo运行时间的测量,重新排序时允许编译器忽略该时间,就像允许忽略来自其他线程的观察一样。
CodesInChaos

Answers:


100

与C ++标准委员会讨论之后,我想尝试提供一个更全面的答案。除了是C ++委员会的成员之外,我还是LLVM和Clang编译器的开发人员。

从根本上讲,无法使用顺序中的障碍或某些操作来实现这些转换。根本问题是实现过程完全了解整数加法之类的操作语义。它可以模拟它们,知道它们不能被正确的程序观察到,并且始终可以随意移动它们。

我们可以尝试防止这种情况发生,但是它会产生极其负面的结果,并最终会失败。

首先,在编译器中防止此错误的唯一方法是告诉它所有这些基本操作都是可观察的。问题在于,这将排除绝大多数编译器优化。在编译器内部,我们基本上没有良好的机制来建模时序是可观察的,但没有别的。我们什至没有一个好的模型来说明哪些操作需要时间。例如,将32位无符号整数转换为64位无符号整数是否需要时间?在x86-64上,它花费零时间,但是在其他体系结构上,它花费非零时间。这里没有一般正确的答案。

但是,即使我们通过一些英勇的技巧成功地阻止了编译器对这些操作进行重新排序,也无法保证这将足够。考虑一种在x86机器上执行C ++程序的有效且一致的方式:DynamoRIO。这是一个动态评估程序机器代码的系统。它可以做的一件事就是在线优化,它甚至能够推测性地在定时之外执行整个基本算术指令范围。而且这种行为并不是动态评估者所独有的,实际的x86 CPU也会推测(数量少得多)指令并对其进行动态重新排序。

基本的认识是,算术是不可观察的(即使在时序级别上),这一事实渗透到了计算机的各个层面。对于编译器,运行时,甚至通常是硬件,都是如此。强制使其可观察会极大地限制编译器,但也会极大地限制硬件。

但是,所有这些都不应该使您失去希望。当您想安排基本数学运算的执行时间时,我们已经研究了可靠工作的技术。通常,在进行微基准测试时会使用这些标记。我在CppCon2015上对此进行了演讲:https ://youtu.be/nXaxk27zwlk

各种微基准库(例如Google的库)也提供了此处显示的技术:https : //github.com/google/benchmark#preventing-optimization

这些技术的关键是关注数据。使输入对优化器不透明,使计算结果对优化器不透明。完成此操作后,就可以对其进行可靠计时了。让我们看一下原始问题中的示例的实际版本,但其定义foo对于实现完全可见。我还DoNotOptimize从Google Benchmark库中提取了一个(非便携式)版本,您可以在这里找到:https : //github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208

#include <chrono>

template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
  asm volatile("" : "+m"(const_cast<T &>(value)));
}

// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto input = 42;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize(input);
  auto output = foo(input);       // Statement 2
  DoNotOptimize(output);
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

在这里,我们确保在计算周围将输入数据和输出数据标记为不可优化foo,并且仅在这些标记周围才计算时序。由于您使用数据来限制计算,因此可以保证将其保留在两个时间之间,但是可以优化计算本身。最新生成的Clang / LLVM生成的x86-64程序集为:

% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
        .text
        .file   "so.cpp"
        .globl  _Z8time_foov
        .p2align        4, 0x90
        .type   _Z8time_foov,@function
_Z8time_foov:                           # @_Z8time_foov
        .cfi_startproc
# BB#0:                                 # %entry
        pushq   %rbx
.Ltmp0:
        .cfi_def_cfa_offset 16
        subq    $16, %rsp
.Ltmp1:
        .cfi_def_cfa_offset 32
.Ltmp2:
        .cfi_offset %rbx, -16
        movl    $42, 8(%rsp)
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, %rbx
        #APP
        #NO_APP
        movl    8(%rsp), %eax
        addl    %eax, %eax              # This is "foo"!
        movl    %eax, 12(%rsp)
        #APP
        #NO_APP
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        subq    %rbx, %rax
        addq    $16, %rsp
        popq    %rbx
        retq
.Lfunc_end0:
        .size   _Z8time_foov, .Lfunc_end0-_Z8time_foov
        .cfi_endproc


        .ident  "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
        .section        ".note.GNU-stack","",@progbits

在这里,您可以看到编译器优化foo(input)了对单个指令的调用addl %eax, %eax,但是尽管有恒定的输入,却没有将其移出时序或完全消除了。

希望这会有所帮助,并且C ++标准委员会正在研究与DoNotOptimize此处类似的标准化API的可能性。


1
谢谢您的回答。我将其标记为新的最佳答案。我本可以做得更早,但是已经有很多个月没有阅读这个stackoverflow页面了。我对使用Clang编译器制作C ++程序非常感兴趣。我尤其喜欢在Clang的变量名中使用Unicode字符。我想我会在Stackoverflow上问更多关于Clang的问题。
S2108887 '12

5
虽然我知道这是如何阻止foo完全优化的,但是您能否详细说明为什么这阻止了Clock::now()相对于foo()的调用重新排序?优化器是否必须假设DoNotOptimizeClock::now()有权访问并可能修改某些通用全局状态,从而将它们绑定到输入和输出?还是您依赖于优化器实施的当前限制?
MikeMB

2
DoNotOptimize在此示例中,是一个合成的“可观察”事件。就像它在名义上将可见输出与输入的表示形式打印到某个终端一样。由于读取时钟也是可以观察到的(您正在观察时间的流逝),因此如果不更改程序的可观察行为,就无法对其重新排序。
钱德勒·卡鲁斯

1
我对“可观察”的概念仍然不太清楚,如果foo函数正在执行某些操作(例如从可能会被阻塞一段时间的套接字读取),这是否算是可观察的操作?而且由于read并不是“完全已知”的操作(对吗?),代码会保持秩序吗?
ravenisadesk

“根本的问题是,像整数加法这样的操作语义对于实现完全是已知的。” 但是在我看来,问题不是整数加法的语义,而是调用函数foo()的语义。除非foo()在同一编译单元中,否则如何知道foo()和clock()不交互?
戴夫

59

摘要:

似乎没有保证防止重新排序的方法,但是只要未启用链接时/全程序优化,将调用的函数放在单独的编译单元中似乎是一个不错的选择。(至少在GCC中,尽管逻辑上也暗示其他编译器也可能这样做。)这是以函数调用为代价的-内联代码根据定义位于同一编译单元中,并且可以重新排序。

原始答案:

GCC对-O2优化下的通话进行重新排序:

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

GCC 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

但:

g++ -S --std=c++11 -O2 fred.cpp

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

现在,使用foo()作为外部函数:

#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

但是,如果与-flto链接(链接时优化),则:

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
MSVC和ICC也是如此。Clang是唯一看起来保留原始序列的语言。
Cody Gray

3
您不在任何地方使用t1和t2,因此可能会认为结果可以丢弃并重新排序代码
phuclv

3
@Niall-我无法提供更多具体信息,但是我认为我的评论暗示了其根本原因:编译器知道foo()不会影响now(),反之亦然,重新排序也是如此。涉及外部作用域函数和数据的各种实验似乎证实了这一点。这包括使静态foo()依赖于文件范围变量N-如果将N声明为静态,则会发生重新排序,而如果将其声明为非静态(即,其他编译单元可见,则可能会受到副作用)诸如now())这样的外部函数不会发生重新排序。
杰里米(Jeremy)

3
@LưuVĩnhPhúc:除了通话本身没有被忽略。再次,我怀疑这是因为编译器不知道它们的副作用可能是什么-但它确实知道那些副作用不会影响foo()的行为。
杰里米(Jeremy)

3
最后一点:指定-flto(链接时优化)会导致重新排序,即使在其他情况下也不会重新排序。
杰里米(Jeremy)2013年

20

重新排序可以由编译器或处理器完成。

大多数编译器提供了特定于平台的方法,以防止对读写指令进行重新排序。在gcc上,这是

asm volatile("" ::: "memory");

更多信息在这里

请注意,这仅间接阻止重新排序操作,只要它们依赖于读/写。

在实践中,我还没有看到这样的系统,该系统调用Clock::now()确实具有与这种屏障相同的效果。您可以检查结果装配以确保。

但是,在编译期间对被测函数进行求值并不罕见。要强制执行“现实”执行,您可能需要foo()从I / O或volatile读取中获取输入。


另一个选择是禁用内联foo()-再次,这是编译器特定的,通常不是可移植的,但是会产生相同的效果。

在gcc上,这将是 __attribute__ ((noinline))


@Ruslan提出了一个基本问题:此度量有多现实?

执行时间受许多因素影响:一个是我们正在运行的实际硬件,另一个是对共享资源(例如缓存,内存,磁盘和CPU内核)的并发访问。

因此,我们通常要做的是获得可比较的时序:确保它们具有低误差余量的可重现性。这使它们有些虚假。

“热缓存”与“冷缓存”的执行性能可以轻易地相差一个数量级-但实际上,这之间是有区别的(“不冷不热”?)


2
您的hack asm会影响定时器调用之间的语句执行时间:内存破坏者之后的代码必须从内存中重新加载所有变量。
Ruslan

@Ruslan:他们的hack,不是我的。清除的级别不同,为获得可重现的结果,不可避免地要进行类似的操作。
peterchen

2
请注意,使用“ asm”进行黑客攻击仅会成为接触内存操作的障碍,OP对此的兴趣不止于此。请参阅我的答案以获取更多详细信息。
钱德勒·卡鲁斯

11

C ++语言通过多种方式定义了可观察的内容。

如果foo()什么都看不到,那么它可以完全消除。如果foo()仅执行以“本地”状态存储值的计算(无论是在堆栈中还是在某个对象中的某个位置),并且编译器可以证明没有安全派生的指针可以进入Clock::now()代码,则不会有可观察到的后果。移动Clock::now()电话。

如果foo()一个文件或显示,编译器交互不能证明Clock::now()确实相互作用与文件或显示,然后重新排序不能这样做,因为文件或显示交互观察到的行为。

虽然您可以使用特定于编译器的技巧来强制代码不移动(例如内联汇编),但另一种方法是尝试使编译器的性能超越智能。

创建一个动态加载的库。在相关代码之前加载它。

该库公开了一件事:

namespace details {
  void execute( void(*)(void*), void *);
}

并像这样包装它:

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

它打包了一个无效的lambda,并使用动态库在编译器无法理解的上下文中运行它。

在动态库中,我们执行以下操作:

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

这很简单。

现在,要重新排序对的调用execute,它必须了解动态库,在编译测试代码时它不能。

它仍然可以消除foo()副作用为零的,但是您赢了一些,却输了一些。


19
“另一种方法是尝试使您的编译器胜于智能”。:-)
Cody Gray

1
我认为可能需要注意的是,代码块执行所需的时间并不被视为编译器需要维护的“可观察”行为。如果执行代码块的时间是“可观察的”,则不允许进行任何形式的性能优化。尽管对于C和C ++定义“因果障碍”会有所帮助,这将要求编译器推迟执行障碍之后的任何代码,直到生成的代码处理障碍之前的所有副作用为止。希望确保数据完全...
超级猫

1
...通过硬件缓存传播,将需要使用特定于硬件的方法来执行此操作,但是没有屏障指令来确保编译器跟踪所有待处理的写操作,而等待所有已发布的写操作完成的特定于硬件的方法将毫无用处必须先将其发布到硬件上,然后再要求硬件以确保完成所有已发布的写volatile操作。
超级猫

4

不,它不能。根据C ++标准[intro.execution]:

14与完整表达式关联的每个值计算和副作用在与要评估的下一个完整表达式关联的每个值计算和副作用之前进行排序。

完整表达式基本上是由分号终止的语句。如您所见,以上规则规定必须按顺序执行语句。这是声明,编译器是允许更多的自由发挥(即它是下允许计算表达式组成比其他订单陈述某些情况下从左向右或任何其他特定的)。

请注意,此处不符合适用规则的条件。认为任何编译器都可以证明重新排序调用以获取系统时间不会影响可观察的程序行为,这是不合理的。如果在某些情况下可以重新排序两次调用时间而不改变观察到的行为,那么实际上产生一个编译器来对程序进行足够的理解就可以确定地推断出它是非常低效的。


12
目前仍然在为-if规则,虽然
MM

18
根据规则,只要编译器不改变可观察的行为,它就可以执行任何代码操作。执行时间不可观察。因此,只要结果相同(大多数编译器会做明智的事情,而不是对时间调用进行重新排序,但这不是必需的),它就可以对代码的辅助代码行进行重新排序
Revolver_Ocelot

6
执行时间不可观察。这很奇怪。从实用,非技术的角度来看,执行时间(也称为“性能”)非常可观。
弗雷德里克·哈米迪

3
取决于您如何测量时间。无法测量执行标准C ++中的某些代码体所花费的时钟周期数。
彼得

3
@dba您正在混合一些东西。链接器无法再生成Win16应用程序,这是很正确的,但这是因为它们已删除了对生成这种类型的二进制文件的支持。WIn16应用程序不使用PE格式。这并不意味着编译器或链接器都具有有关API函数的特殊知识。另一个问题与运行时库有关。获得最新版本的MSVC来生成在NT 4上运行的二进制文件绝对没有问题。我已经做到了。当您尝试链接CRT(调用不可用的功能)时,就会出现问题。
科迪·格雷

2

没有。

有时,根据“如果”规则,可以对语句进行重新排序。这不是因为它们在逻辑上是彼此独立的,而是因为这种独立性允许在不更改程序语义的情况下进行这种重新排序。

移动获得当前时间的系统调用显然不满足该条件。有意或无意地这样做的编译器是不兼容的,而且确实很愚蠢。

总的来说,我不希望任何导致系统调用的表达式都被积极优化的编译器“误导”。只是对系统调用的作用还不了解。


5
我同意这是愚蠢的,但是我不认为这是不合格的。编译器可以知道具体系统上的系统调用到底能做什么以及是否有副作用。我希望编译器不会仅仅为了覆盖常见的用例而重新排序此类调用,以提供更好的用户体验,而不是因为标准禁止这样做。
Revolver_Ocelot

4
@Revolver_Ocelot:更改程序语义的优化(好的,保存副本省略)不符合该标准,无论您是否同意。
Lightness Races in Orbit

6
在琐碎的情况下,int x = 0; clock(); x = y*2; clock();没有为定义的方式clock()代码与状态进行交互x。在C ++标准下,它不必知道做什么clock()-它可以检查堆栈(并注意何时进行计算),但这不是C ++的问题
Yakk-Adam Nevraumont

5
进一步说明Yakk的观点:的确,对系统调用进行重新排序是正确的,因此,如果使用这些值,则将第一个分配给t2,将第二个分配给,这t1将是不合格且愚蠢的,这个答案遗漏的是合格的编译器有时可以在整个系统调用中重新排序其他代码。在这种情况下,只要知道foo()执行的操作(例如,因为已对其进行内联),并且因此(松散地说)它是一个纯函数,则可以将其移动。
史蒂夫·杰索普

1
再次松散地说,这是因为不能保证实际的实现(尽管不是抽象机)y*y在系统调用之前不会进行推测性的计算,只是为了好玩。也不能保证实际的实现以后在任何时候都不会使用这种推测性计算的结果x,因此在对的调用之间什么也不做clock()。内联函数foo所做的任何事情也一样,只要它没有副作用且不能依赖可能被改变的状态即可clock()
史蒂夫·杰索普

0

noinline 功能+内联汇编黑盒+完整的数据依赖项

这是基于https://stackoverflow.com/a/38025837/895245的,但是因为我看不到为何::now()无法在此处重新排序的明确理由,所以我偏执地将其与noinline函数一起放在组装

这样,我非常确定不会发生重新排序,因为noinline“绑定”了the ::now和数据依赖项。

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub上游

编译并运行:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

该方法的唯一次要缺点是我们在方法上添加了一条额外的callq指令inlineobjdump -CD显示main包含:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

所以我们看到它foo是内联的,但get_clock不是内联的。

get_clock 但是,它本身非常高效,由一个甚至不涉及堆栈的单叶调用优化指令组成:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

由于时钟精度本身是有限的,因此,我认为您不太可能注意到额外的时钟效应jmpq。请注意,call无论::now()是共享库中的内容还是必需的,都是必需的。

::now()从具有数据依赖性的内联程序集调用

这将是可能的最有效解决方案,甚至可以克服jmpq上面提到的其他问题。

不幸的是,这很难正确完成,如下所示:在扩展内联ASM中调用printf

如果您的时间测量可以直接在联机汇编中完成而无需致电,则可以使用此技术。例如,gem5魔术仪器指令,x86 RDTSC(不确定是否能代表)和其他性能计数器就是这种情况。

相关主题:

已在GCC 8.3.0,Ubuntu 19.04中测试。


1
通常,您不需要使用强制溢出/重新加载"+m",使用,"+r"是使编译器具体化一个值然后假定变量已更改的更有效的方法。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.