【问题标题】:Using Assembly Language in C/C++在 C/C++ 中使用汇编语言
【发布时间】:2011-05-11 07:15:12
【问题描述】:

我记得在某处读到,为了真正优化和加速代码的某些部分,程序员用汇编语言编写该部分。我的问题是 -

  1. 这种做法还在做吗?以及如何做到这一点?
  2. 用汇编语言编写是不是有点过于繁琐和陈旧?
  3. 当我们编译 C 代码(带或不带 -O3 标志)时,编译器会进行一些代码优化并链接所有库并将代码转换为二进制目标文件。因此,当我们运行程序时,它已经是最基本的形式,即二进制。那么引入“汇编语言”有什么帮助呢?

我正在努力理解这个概念,非常感谢任何帮助或链接。

更新: 根据 dbemerlin 的要求重新表述第 3 点 - 因为您可能能够编写比编译器生成的更有效的汇编代码,但除非您是汇编专家,否则您的代码可能会运行得更慢,因为通常编译器比大多数人更能优化代码。

【问题讨论】:

  • 好问题,措辞正确。 +1
  • 这可能是关于 SO 的仅有的五个问题之一,其中使用“C/C++”是有意义的。

标签: c++ c optimization compiler-optimization assembly


【解决方案1】:

恢复到汇编语言唯一有用的时候是

...和...

  • 使用这些 CPU 指令可以显着提高瓶颈代码的性能。

简单地使用内联汇编来做一个可以很容易用 C++ 表达的操作——比如添加两个值或在一个字符串中搜索——会适得其反,因为:

  • 编译器同样知道如何做到这一点
    • 要验证这一点,请查看其汇编输出(例如gcc -S)或反汇编机器代码
  • 您人为地限制了它在寄存器分配、CPU 指令等方面的选择,因此可能需要更长的时间为 CPU 寄存器准备执行硬编码指令所需的值,然后更长时间才能恢复到未来的最佳分配指示
    • 编译器优化器可以在指定不同寄存器的等效性能指令之间进行选择,以最大程度地减少它们之间的复制,并且可以选择寄存器以使单个内核可以在一个周期内处理多条指令,而强制通过特定寄存器将所有内容序列化
      • 公平地说,GCC 有办法表达对特定类型寄存器的需求,而无需将 CPU 限制为精确的寄存器,仍然允许进行此类优化,但它是我见过的唯一解决此问题的内联汇编
  • 如果明年推出的新 CPU 型号带有另一条指令,该指令对于相同的逻辑操作要快 1000%,那么编译器供应商更有可能更新他们的编译器以使用该指令,因此您的程序在重新编译后会受益,比你现在(或当时维护软件的人)
  • 编译器将为其告知的目标架构选择一种最佳方法:如果您硬编码一个解决方案,那么它需要是适用于您的平台的最低公分母或#ifdef-ed
  • 汇编语言不像 C++ 那样可移植,无论是跨 CPU 还是跨编译器,即使您看似移植了一条指令,也可能在重新注册可安全破坏的寄存器、参数传递约定等方面出错。李>
  • 其他程序员可能不知道或不熟悉汇编

我认为值得牢记的一个观点是,当 C 被引入时,它必须赢得许多对生成的机器代码大惊小怪的铁杆汇编语言程序员。那时机器的 CPU 能力和 RAM 更少,你可以打赌人们会为最微小的事情大惊小怪。优化器变得非常复杂并不断改进,而像 x86 这样的处理器的汇编语言变得越来越复杂,它们的执行管道、缓存和其他与性能有关的因素也变得越来越复杂。您不能再从每条指令的周期表中添加值。编译器编写者花时间考虑所有这些微妙的因素(尤其是那些为 CPU 制造商工作的因素,但这也增加了其他编译​​器的压力)。现在,对于汇编程序员来说,在任何非平凡的应用程序上平均代码效率明显高于由良好的优化编译器生成的代码效率是不切实际的,而且他们极有可能做得更糟。因此,组件的使用应仅限于真正产生可衡量和有用的差异的时间,值得耦合和维护成本。

【讨论】:

    【解决方案2】:

    首先,您需要对您的程序进行概要分析。然后优化 C 或 C++ 代码中最常用的路径。 Unless advantages are clear you don't rewrite in assembler。使用汇编程序会使您的代码更难维护且可移植性大大降低 - 除非在极少数情况下,否则不值得这样做。

    【讨论】:

    • 介绍我的程序?你的意思是这可以帮助我决定是否要使用 Assembly?
    • @MovieYoda:不,它可以帮助您找出瓶颈所在。这样,您就不会浪费时间尝试优化一段甚至不是性能的主要因素的代码。通常,使用 C 或 C++ 代码编写程序集只能作为最后的手段。通常,仅使用不同的算法或数据结构就可以加快代码速度。
    • 是的,因为它会告诉您您的程序大部分时间都花在了哪里,并且会从优化中受益。但是,您应该看看您的代码是否会受益于比蛮力汇编程序更好的算法。
    • @MovieYoda:是的,您可能会发现一些愚蠢的代码,只需重写它们(仍然使用 C 或 C++)就会产生巨大的推动作用。例如,如果您在循环中调用strlen(),而字符串长度没有改变,那么在汇编程序中重写是浪费时间——您只需使用一个临时变量来存储长度,并且(神奇!)您的程序可能运行得更快。
    • @MovieYoda:这是我做的一篇文章 (stackoverflow.com/questions/926266/…),展示了如何找到真正值得优化的代码,而循环压缩(如编写 asm)几乎从来都不是需要的。跨度>
    【解决方案3】:

    (1) 是的,最简单的尝试方法是使用内联汇编,这取决于编译器,但通常看起来像这样:

    __asm
    {
        mov eax, ebx
    }
    

    (2) 这是非常主观的

    (3) 因为您可能能够编写比编译器生成的更有效的汇编代码。

    【讨论】:

    • 酷!所以这种做法被称为'内联汇编'!很好......所以基本上,这种做法严重依赖硬件和平台?因为每个硬件和平台的指令集都有微小的变化?
    • 您可能希望将 (3) 更改为 Because you might be able to write more effective assembly code than the compiler generates,但除非您是汇编专家,否则您的代码运行速度可能会更慢,因为编译器通常会比大多数人更好地优化代码。
    • 我认为“可能”涵盖了它,我认为你不能比这更量化。
    • 我不同意(1)。最简单的方法通常是使用“离线”汇编源文件。通过这种方式,您可以获得正确的语法突出显示,并且可以使用为人类设计的具有有用功能(例如更强大的宏)的汇编程序。我通常推荐yasm。
    • @dbemerlin 您无需成为专家即可优化编译器生成的代码。你只需要找到正确的位置,并且知道编译器没有考虑到的东西。查看生成的代码是最好的。通常,您会发现编译器会在不需要此类保护的地方进行保护。在循环的核心跳过一个负载,可能会在代码中的正确位置产生奇迹。
    【解决方案4】:

    您应该阅读经典书籍Zen of Code OptimizationMichael Abrash 的后续Zen of Graphics Programming

    概括地说,在第一本书中,他解释了如何将汇编编程推向极限。在后续的文章中,他解释说程序员应该使用一些更高级的语言,比如 C,并且只尝试使用汇编来优化非常具体的地方,如果有必要的话。

    这种改变想法的一个动机是,他发现,与从高级语言编译的代码(可能是编译器例如,使用新指令,或现有指令的性能和行为从一代处理器更改为另一代)。

    另一个原因是编译器非常好,并且现在正在积极优化,通常在将 C 代码转换为汇编的算法上可以获得更多的性能。即使对于 GPU(图形卡处理器)编程,您也可以使用 cuda 或 OpenCL 使用 C 来完成。

    仍有一些(极少数)情况下您应该/必须使用汇编,通常是为了对硬件进行非常精细的控制。但即使在 OS 内核代码中,它通常也是非常小的部分,而且代码不多。

    【讨论】:

    • 不只是使用 new 指令会有所作为。调整选项,例如是否展开/展开多少,使用哪些指令(loop vs. dec/jnzsub/mov vs. push)在 8086 和 686 之间发生了巨大变化。和 586 个有序超标量pentium 是一个异常值,它可以流水线化简单的指令,因此值得使用更简单的指令而不是更少的复杂指令。后来的 CPU 可以将复杂的 CPU 解码为多个微指令,但 586 不能而且只会停止管道。
    • 此外,针对 8086 进行调整 = 通常会最小化代码大小,因为指令获取是 的主要瓶颈。为现代 x86 进行调整 = 最小化 uop 计数和依赖链的延迟。无论如何是的,除非您需要为一组有限的 CPU 微架构调整一个热循环,否则您不需要手写 asm。编译器非常好,但肯定确实错过了所有地方的优化。但通常非常小,特别是如果您在现代 x86 上运行,并且管道很宽,以消耗浪费的指令,因此您仍然主要是内存瓶颈。
    【解决方案5】:

    如今使用汇编语言的理由很少,即使是像 SSE 和较旧的 MMX 这样的低级结构在 gcc 和 MSVC 中都有内置的内在函数(我敢打赌 icc 也是,但我从未使用过它)。

    老实说,如今的优化器非常激进,以至于大多数人在汇编代码中编写代码的性能甚至达不到一半。您可以更改数据在内存中的排序方式(针对局部性)或告诉编译器更多关于您的代码的信息(通过#pragma),但实际上是在编写汇编代码......怀疑您是否会从中得到任何额外的东西。

    @VJo,请注意,在高级 C 代码中使用内部函数可以让您进行相同的优化,而无需使用单个汇编指令。

    对于它的价值,已经讨论了下一个 Microsoft C++ 编译器,以及他们将如何从中删除内联汇编。这充分说明了对它的需求。

    【讨论】:

      【解决方案6】:

      我认为您没有指定处理器。不同的答案取决于处理器和环境。一般的答案是肯定的,它仍然完成,它肯定不是过时的。一般的原因是编译器,有时它们在一般优化方面做得很好,但对于特定目标却不是很好。有些人在一个目标上非常擅长,而在其他目标上却不太擅长。大多数时候它已经足够好了,大多数时候你想要可移植的 C 代码而不是不可移植的汇编程序。但是您仍然会发现 C 库仍然会手动优化 memcpy 和其他编译器根本无法弄清楚有一种非常快速的方法来实现它的例程。部分原因是这种极端情况不值得花时间优化编译器,只需在汇编程序中解决它,并且构建系统有很多 if this target then use C if that target use C if that target use asm, if that目标使用 asm。所以它仍然会发生,我认为必须在某些领域永远持续下去。

      X86 是自己的野兽,有很多历史,我们正处于这样一个阶段,你真的不能以实际的方式编写一个总是更快的汇编程序,你绝对可以优化特定机器上特定处理器的例程在特定的一天,并执行编译器。除了某些特定情况外,它通常是徒劳的。有教育意义,但总的来说不值得花时间。还要注意处理器不再是瓶颈,所以一个草率的通用 C 编译器就足够了,在别处寻找性能。

      其他平台,通常是指嵌入式、arm、mips、avr、msp430、pic 等。您可能运行也可能不运行操作系统,可能运行也可能不运行缓存或桌面上的其他类似东西已。所以编译器的弱点就会显现出来。另请注意,编程语言继续远离处理器而不是向处理器发展。即使在 C 被认为是低级语言的情况下,它也不匹配指令集。总会有一些时候,您可以生成优于编译器的汇编程序段。不一定是您的瓶颈部分,但在整个程序中,您通常可以在这里和那里进行改进。您仍然必须检查这样做的价值。在嵌入式环境中,它可以而且确实决定了产品的成败。如果您的产品每单位 25 美元投资于更耗电、电路板空间、更高速度的处理器,因此您不必使用汇编程序,但您的竞争对手每单位花费 10 美元或更少,并且愿意将 asm 与 C 混合以使用更小的内存,使用更少的电力,更便宜的零件等。只要 NRE 被回收,那么从长远来看,与 asm 混合的解决方案将。

      真正的嵌入式是一个拥有专业工程师的专业市场。另一个嵌入式市场,你的嵌入式 linux roku、tivo 等。嵌入式手机等都需要有便携式操作系统才能生存,因为你需要第三方开发人员。所以这个平台必须更像一个桌面而不是一个嵌入式系统。埋在提到的 C 库或操作系统中可能会有一些汇编程序优化,但是对于桌面,您想尝试投入更多硬件,以便软件可以移植而不是手动优化。如果第三方成功需要汇编程序,您的产品线或嵌入式操作系统将失败。

      我最担心的是这些知识正在以惊人的速度流失。因为没有人检查汇编程序,因为没有人用汇编程序编写代码,等等。没有人注意到编译器在生成代码时没有得到改进。开发人员通常认为他们必须购买更多硬件,而不是意识到通过了解编译器或如何更好地编程,他们可以使用相同的编译器将性能提高 5% 到数百%,有时使用相同的源代码。 5-10% 通常使用相同的源代码和编译器。 gcc 4 并不总是产生比 gcc 3 更好的代码,我保留两者,因为有时 gcc3 做得更好。目标特定的编译器可以(并非总是如此)围绕 gcc 运行,有时使用相同的源代码不同的编译器可以看到百分之几百的改进。这一切从何而来?仍然费心寻找和/或使用汇编程序的人。其中一些人在编译器后端工作。前端和中间当然是有趣和有教育意义的,但后端是你决定或破坏最终程序的质量和性能的地方。即使您从不编写汇编程序,而只是不时查看编译器的输出(gcc -O2 -s myprog.c),它也会使您成为更好的高级程序员并保留其中的一些知识。如果没有人愿意知道和编写汇编程序,那么根据定义,我们已经放弃编写和维护用于高级语言和软件的编译器了。

      了解以 gcc 为例,编译器的输出是汇编,然后传递给汇编器,汇编器将其转换为目标代码。 C 编译器通常不会生成二进制文件。对象组合到最终二进制文件中时,由链接器完成,链接器是另一个由编译器调用的程序,而不是编译器的一部分。编译器将 C 或 C++ 或 ADA 或其他任何东西转换为汇编器,然后汇编器和链接器工具将其完成剩下的工作。动态重新编译器,例如 tcc,必须能够以某种方式动态生成二进制文件,但我认为这是例外而不是规则。 LLVM 有自己的运行时解决方案,如果您将其用作交叉编译器,则可以非常明显地显示内部代码的高级到目标代码到二进制路径。

      所以回到正题,是的,它已经完成了,比你想象的更频繁。主要与不直接与指令集比较的语言有关,然后编译器并不总是产生足够快的代码。如果您可以说对 malloc 或 memcpy 等大量使用的功能进行了数十倍的改进。或者想在没有硬件支持的情况下在手机上拥有高清视频播放器,平衡组装机的利弊。真正的嵌入式市场仍然相当多地使用汇编程序,有时全是 C,但有时软件完全用汇编程序编码。对于桌面 x86,处理器不是瓶颈。处理器是微编码的。即使您在表面上制作漂亮的汇编程序,它也不会在所有系列的 x86 处理器上运行得非常快,草率的、足够好的代码更有可能全面运行大致相同的代码。

      我强烈建议学习用于非 x86 ISA 的汇编程序,例如 arm、thumb/thumb2、mips、msp430、avr。具有编译器的目标,尤其是具有 gcc 或 llvm 编译器支持的目标。学习汇编程序,学习理解 C 编译器的输出,并通过实际修改输出和测试来证明你可以做得更好。这些知识将有助于使您的桌面高级代码在没有汇编程序的情况下变得更好、更快、更可靠。

      【讨论】:

      • 好吧,我不是在寻找任何特定的处理器。我想了解这种做法以及采取这种方法的原因。只是更新我的知识...
      【解决方案7】:

      这取决于。在某些情况下(仍然)正在这样做,但在大多数情况下,这是不值得的。现代 CPU 异常复杂,为它们编写高效的汇编代码同样复杂。所以大多数时候,你手工编写的程序集最终会比编译器为你生成的程序要慢。

      假设最近几年发布了一个不错的编译器,您通常可以调整您的 C/C++ 代码以获得与使用汇编相同的性能优势。

      这里的 cmets 和答案中的很多人都在谈论他们在汇编中重写某些东西获得的“N 倍加速”,但这本身并不意味着太多。我重写了一个评估流体动力学方程的 C 函数在 C 中的速度提高了 13 倍,通过应用许多与在汇编中编写它时相同的优化,通过了解硬件,以及通过分析。最后,它已经足够接近 CPU 的理论峰值性能,因此在汇编中重写它是没有意义的。通常,限制因素不是语言,而是您编写的实际代码。只要您不使用编译器难以处理的“特殊”指令,就很难击败编写良好的 C++ 代码。

      Assembly 的速度并不神奇。它只是将编译器带出循环。这通常是一件坏事,除非您真的知道自己在做什么,因为编译器会执行很多手动操作非常痛苦的优化。但是在极少数情况下,编译器只是不理解您的代码,并且无法为其生成有效的汇编,那么,您自己编写一些汇编可能会很有用。除了驱动程序开发等(您需要直接操作硬件)之外,我能想到的唯一值得编写程序集的地方是,如果您遇到无法从中生成高效 SSE 代码的编译器内在函数(例如 MSVC)。即使在那里,我仍然会开始在 C++ 中使用内在函数,并对其进行分析并尝试尽可能地对其进行调整,但是由于编译器在这方面不是很擅长,因此重写该代码最终可能是值得的在装配中。

      【讨论】:

        【解决方案8】:

        看看here,他使用汇编代码将性能提高了 6 倍。所以,答案是:它还在做,但编译器做得很好。

        【讨论】:

        • 所以你的意思是编译器足够好但是如果编译器无法优化某些部分然后使用汇编?
        • @VJo:请注意,本文介绍了通过处理器指令集优化数学密集型例程。在这种特定情况下,编写汇编可能是一种好处,但在一般情况下则不然。
        • @MovieYoda:没有编译器可以帮助处理非常愚蠢的代码 - 首先分析程序并尝试在没有汇编程序的情况下对其进行优化。
        • @MovieYoda:对于一些非常特殊的情况,可以利用可用的硬件。然而,一般来说,用 C++ 编写内联汇编并不经常完成,因为编译器在优化代码方面做得足够好(假设非 WTF 代码),而且更聪明的编译器有时可能比手动优化更好,因为优化可能非常违反直觉。
        • @sharptooth 明白了。喜欢你们分享的链接。
        【解决方案9】:
        1. “这种做法还在做吗?” --> 在图像处理、信号处理、人工智能(例如高效矩阵乘法)等领域完成。我敢打赌,我的 macbook 触控板上的滚动手势处理也是部分汇编代码,因为它是即时的。 --> 甚至可以在 C# 应用程序中完成(参见 https://blogs.msdn.microsoft.com/winsdk/2015/02/09/c-and-fastcall-how-to-make-them-work-together-without-ccli-shellcode/

        2. “用汇编语言编写是不是有点太麻烦和过时了?” --> 它是一种类似于锤子或螺丝刀的工具,有些任务需要制表螺丝刀。

          1. “当我们编译 C 代码(带或不带 -O3 标志)时,编译器会进行一些代码优化……那么引入‘汇编语言’有何帮助?” --> 我喜欢@jalf 所说的,以编写汇编的方式编写C 代码已经可以产生高效的代码。但是要做到这一点,您必须考虑如何用汇编语言编写代码,例如。了解复制数据的所有地方(每次不必要时都会感到痛苦)。 使用汇编语言,您可以确定生成了哪些指令。即使您的 C 代码是高效的,也不能保证生成的程序集对于每个编译器都是高效的。 (见https://lucasmeijer.com/posts/cpp_unity/) --> 使用汇编语言,当您分发二进制文件时,您可以测试 cpu 并根据针对 AVX 或仅针对 SSE 优化的 cpu 功能创建不同的分支,但您只需要分发一个二进制文件。使用内在函数,这在 C++ 或 .NET Core 3 中也是可能的。(请参阅https://devblogs.microsoft.com/dotnet/using-net-hardware-intrinsics-api-to-accelerate-machine-learning-scenarios/

        【讨论】:

          【解决方案10】:

          在我的工作中,我使用嵌入式目标(微控制器)上的程序集进行低级访问。

          但是对于一个PC软件来说,我觉得用处不大。

          【讨论】:

          • 我认为现在只有游戏程序员在编程中使用 ASM。
          • 啊啊!现在我记得我在哪里读到了这种做法。它是用汇编写的关于游戏“极品飞车”的。我自然是惊呆了
          • @graham.reeds:几年前确实如此,但对于像 CUDA 这样的 GPU 层,我不确定对于游戏程序员来说是否仍然如此。内核或驱动程序编程,或一些嵌入式设备仍有一些小地方。
          • @Kriss:Assembler 将始终用于游戏开发。不管怎样,使用汇编程序在包括 PC 在内的任何平台上都非常有用。我有一些音频卷积代码,我在汇编程序中重新编写了这些代码,并且在直接 C 中得到了 5 倍的卷积速度。
          • @Goz:当我听到总是时,我已经足够大了,我会很怀疑。使用 assembler 一段时间可能仍然有用,但你不应该把赌注押在未来。现在即使是在游戏中,也很少有人使用游戏引擎(其中汇编很有用),并且在许多游戏中使用相同的引擎。当优化变得足够困难时,您将获得Duke Nukem Forever 效果。您尚未完成对下一代硬件的优化,您必须从头开始,因为一切都改变了,您的旧优化代码现在比新硬件上的编译代码效率低......
          【解决方案11】:

          我有一个我已经完成的程序集优化示例,但它还是在嵌入式目标上。你也可以看到一些用于 PC 的汇编编程示例,它创建了非常小而快速的程序,但通常不值得付出努力(寻找“用于 windows 的汇编”,你可以找到一些非常小而漂亮的程序)。

          我的示例是在编写打印机控制器时,有一个函数应该每 50 微秒调用一次。它必须或多或少地重新洗牌。使用 C 语言我已经能够在大约 35 微秒内完成它,而使用汇编我已经在大约 8 微秒内完成了它。这是一个非常具体的程序,但仍然是真实且必要的。

          【讨论】:

            【解决方案12】:

            在某些嵌入式设备(电话和 PDA)上,它很有用,因为编译器并不十分成熟,并且可能生成极慢甚至不正确的代码。我个人不得不解决或编写汇编代码来修复基于 ARM 的嵌入式平台的几种不同编译器的错误输出。

            【讨论】:

              【解决方案13】:
              1. 是的。使用内联汇编或链接汇编对象模块。您应该使用哪种方法取决于您需要编写多少汇编代码。通常可以使用几行内联汇编,如果有多个函数,则切换到单独的对象模块一次。
              2. 当然,但有时是必要的。这里最突出的例子是对操作系统进行编程。
              3. 当今的大多数编译器都对您用高级语言编写的代码进行了优化,比任何人编写的汇编代码都要好得多。人们大多使用它来编写原本不可能用 C 等高级语言编写的代码。如果有人将它用于其他任何事情,则意味着他要么比现代编译器更擅长优化(我对此表示怀疑),要么就是愚蠢,例如他不知道要使用什么编译器标志或函数属性。

              【讨论】:

              • 如果有人真的编写汇编代码是为了优化某个代码片段以提高速度,他必须知道代码将在哪个 CPU 上运行以及这个特定 CPU 在内部如何工作。大多数现代 CPU 能够通过分析哪些指令不依赖于其他指令的结果以及许多其他加速程序执行的方法来同时(在一个内核中)执行多条指令。
              • 还有一些人(很可能不是问这种问题的人)知道 cpus 的内部结构,为不同类型的 cpus 编写不同的代码路径,并且实际上能够生成比任何编译器。有关一些有趣的东西,请参阅agner.org/optimize。因此,用“大多数人”替换答案的(3)中的“任何人”会更正确。
              • 我认为你高估了编译器理解程序的能力。是的,编译器会知道如何改组指令以优化管道的使用。但它对哪些变量/函数相互依赖知之甚少。正因为如此,您可以,而且我团队中的其他人编写的汇编代码优于编译器。
              【解决方案14】:

              使用这个:

              __asm__ __volatile__(/*assembly code goes here*/);

              __asm__ 也可以是 asm。

              __volatile__ 阻止编译器进行进一步优化。

              【讨论】:

              • 欢迎来到 SO!请阅读游览tourHow to Answer 一个问题。这个问题是 10 年前提出的,并已被接受。
              • GNU C Basic asm(只有一串指令,没有输入/输出/clobbers 约束)已过时且危险,不能安全地用于任何事情。见gcc.gnu.org/wiki/ConvertBasicAsmToExtended。例如通过符号名称引用全局变量是不安全的,修改任何寄存器也不安全,并且(在 x86-64 中)也不使用任何堆栈空间(除非您跳过红色区域)。永远不要在函数中使用它。有关 GNU C 扩展汇编的指南,请参阅 stackoverflow.com/tags/inline-assembly/info。 (例如asm ("add %1, %0" : "+r"(var) : "r"(var))
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-09-16
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多