高性能编程技巧与算法优化深度解析
在编程的世界里,性能优化是一个永恒的话题。无论是处理大规模数据还是追求极致的运行速度,优化代码性能都是必不可少的。本文将深入探讨一系列高性能编程的技巧和算法优化方法,结合具体的代码示例和实际测试结果,为大家展示如何在不同场景下提升程序的运行效率。
循环优化技巧
在编程中,循环是一个常用的结构,但不合理的循环使用可能会导致性能瓶颈。下面介绍几种常见的循环优化方法。
合并循环(Merge Loops)
当有两个
for
循环遍历相同的值序列且循环之间没有依赖关系时,合并循环是一个明智的选择。例如:
for (i = 0; i < 1000; i++) a[i] = b[i] + c[i];
for (j = 0; j < 1000; j++) d[j] = b[j] - c[j];
可以合并为:
for (i = 0; i < 1000; i++) {
a[i] = b[i] + c[i];
d[i] = b[i] - c[i];
}
合并循环可以增加循环体的大小,减少开销百分比,并有助于保持流水线的满负荷运行。此外,还可以避免重复加载
b
和
c
的值,从而提高性能。
拆分循环(Split Loops)
与合并循环相反,当一个循环处理两个独立的数据集,且合并后的循环超出缓存容量时,拆分循环可能会提高性能。但这需要在更好的缓存使用和流水线中更多的指令之间进行权衡,有时合并更好,有时拆分更好。
交换循环(Interchange Loops)
在处理二维数组时,循环的顺序会影响性能。例如,在C语言中,第二个索引的递增速度比第一个快。当CPU将数据提取到缓存中时,会提取多个字节,缓存写入内存的行为也是类似的。因此,下面的第一个循环更有意义:
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
X[i][j] = 0;
}
}
而第二个循环:
for (j = 0; j < n; j++) {
for (i = 0; i < n; i++) {
x[i][j] = 0;
}
}
如果数组太大,第二个版本可能会导致虚拟内存抖动,每次数组访问都可能变成磁盘访问。
移动循环不变代码(Move Loop Invariant Code Outside Loops)
将循环中不变的代码移到循环外部是一种常见的优化方法。通过研究编译器生成的代码,可能会发现一些被忽略的循环不变代码。
移除递归(Remove Recursion)
如果可以轻松消除递归,通常会提高效率。对于“尾”递归,即函数的最后一个动作是递归调用,可以通过跳转到函数顶部来消除。但对于像快速排序这样进行两个非平凡递归调用的函数,消除递归可能需要使用自己的栈来“模拟”递归,这可能会使程序变慢。不过,由于快速排序中进行递归调用的时间较少,所以影响通常较小。
消除栈帧(Eliminate Stack Frames)
对于叶子函数,不需要使用栈帧。如果非叶子函数只调用自己的函数,也可以省略帧指针。帧指针主要用于调试,只有在函数有参与对齐16或32字节访问的局部变量(在栈上)时,栈帧对齐才会成为问题。如果知道自己的代码不使用这些指令,那么除了调试之外,帧指针和帧对齐都不重要。
内联函数(Inline Functions)
编译器可以将小函数内联,以显著减少开销。如果想自己实现内联,可以探索宏,它可以使代码更易读和编写,并且操作起来就像内联函数一样。
减少依赖以允许超标量执行(Reduce Dependencies to Allow Super-scalar Execution)
现代CPU会提前检查指令流,寻找不依赖于早期指令结果的指令,这称为“乱序执行”。如果代码中的依赖关系较少,CPU将能够更高效地执行更多指令,从而使程序运行得更快。例如,修改之前的
add_array
函数,将循环中的所有4个值累加到
rax
中,会使执行时间从2.44秒增加到2.75秒。
使用专门指令(Use Specialized Instructions)
x86 - 64架构中有许多专门指令,编译器可能难以应用这些指令。人类可以重新组织算法,利用这些专门指令来提高性能。例如,可以使用AVX寄存器同时保存4个部分和,在循环结束后再将它们合并,这样可以在一条指令中完成4次加法,从而加快计算速度。这种技术还可以与循环展开结合使用,以进一步提高性能。
数组中计数位的算法优化
在处理数组时,计数数组中所有1位的问题是一个常见的需求。下面介绍几种不同的解决方案。
C函数实现
long popcnt_array (long *a, int size) {
int w, b;
long word;
long n;
n = 0;
for (w = 0; w < size; w++) {
word = a[w];
n += word & 1;
for (b = 1; b < 64; b++) {
n += (word >> b) & 1;
}
}
return n;
}
这个简单的C函数通过双重循环遍历数组中的每个元素,并逐位检查是否为1。在不同的优化级别下进行测试,结果如下:
| 优化级别 | 执行时间(秒) |
| ---- | ---- |
| -O0 | 14.63 |
| -O1 | 5.29 |
| -O2 | 5.29 |
| -O3 | 5.37 |
| -O3 -funroll-all-loops | 4.74 |
通过观察发现,被测试的四字的高位可能经常为0,因此可以将内层的
for
循环改为
while
循环:
long popcnt_array (unsigned long *a, int size) {
int w;
unsigned long word;
long n;
n = 0;
for (w = 0; w < size; w++) {
word = a[w];
while (word != 0) {
n += word & 1;
word >>= 1;
}
}
return n;
}
使用最大优化选项时,这个版本的执行时间为3.34秒,这是使用更好算法的一个实例。
汇编实现
在汇编代码中,可以将处理64位的循环展开为64个处理1位的步骤。以下是一个示例:
segment .text
global popcnt_array
popcnt_array:
push rbx
push rbp
push r12
push r13
push r14
push r15
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
xor r12d, r12d
xor r13d, r13d
xor r14d, r14d
xor r15d, r15d
.count_words:
mov r8, [rdi]
mov r9, r8
mov r10, r8
mov r11, r9
and r8, 0xffff
shr r9, 16
and r9, 0xffff
shr r10, 32
and r10, 0xffff
shr r11, 48
and r11, 0xffff
mov r12w, r8w
and r12w, 1
add rax, r12
mov r13w, r9w
and r13w, 1
add rbx, r13
mov r14w, r10w
and r14w, 1
add rcx, r14
mov r15w, r11w
and r15w, 1
add rdx, r15
%rep 15
shr r8w, 1
mov r12w, r8w
and r12w, 1
add rax, r12
shr r9w, 1
mov r13w, r9w
and r13w, 1
add rbx, r13
shr r10w, 1
mov r14w, r10w
and r14w, 1
add rcx, r14
shr r11w, 1
mov r15w, r11w
and r15w, 1
add rdx, r15
%endrep
add rdi, 8
dec rsi
jg .count_words
add rax, rbx
add rax, rcx
add rax, rdx
pop r15
pop r14
pop r13
pop r12
pop rbp
pop rbx
ret
这个汇编代码将每个字的四分之一位分别放在不同的寄存器中,然后使用不同的寄存器进行累加,这样可以让计算机在循环中更自由地使用乱序执行。该版本的测试执行时间为2.52秒。
预计算每个字节的位数
可以预计算每个可能的位模式中的位数,并使用一个256字节的数组来存储每个字节的位数。计算一个四字中的位数时,只需将四字的8个字节作为索引,从数组中取出对应的位数并相加。以下是C函数实现:
long popcnt_array (long *a, int size) {
int b;
long n;
int word;
n = 0;
for (b = 0; b < size*8; b++) {
word = ((unsigned char *) a)[b];
n += count[word];
}
return n;
}
这个代码的测试执行时间为0.24秒,是目前最快的算法。尝试使用汇编语言来击败这个算法,但只实现了平局。
使用
popcnt
指令
Core i系列处理器中包含的
popcnt
指令可以直接给出64位寄存器中1位的数量。以下是使用
popcnt
指令的汇编代码:
segment .text
global popcnt_array
popcnt_array:
xor eax, eax
xor r8d, r8d
xor ecx, ecx
.count_more:
popcnt rdx, [rdi+rcx*8]
add rax, rdx
popcnt r9, [rdi+rcx*8+8]
add r8, r9
add ecx, 2
cmp ecx, rsi
jl .count_more
add rax, r8
ret
在Core i7上,这个版本的执行时间为0.04秒,比之前的算法快6倍。
通过以上的分析和示例,我们可以看到不同的优化方法和算法在不同场景下的性能表现。在实际编程中,我们需要根据具体的需求和场景选择合适的优化方法,以达到最佳的性能。
图像边缘检测的Sobel滤波器优化
Sobel滤波器是图像处理中常用的边缘检测滤波器,它通过对3x3窗口的数据进行卷积操作,计算图像在x和y方向的边缘度量。
Sobel滤波器的C实现
#include <math.h>
#define matrix(a,b,c) a[(b)*(cols)+(c)]
void sobel (unsigned char *data, float *output, long rows, long cols) {
int r, c;
int gx, gy;
for (r = 1; r < rows-1; r++) {
for (c = 1; c < cols-1; c++) {
gx = -matrix(data,r-1,c-1) + matrix(data,r-1,c+1) +
-2*matrix(data,r,c-1) + 2*matrix(data,r,c+1) +
-matrix(data,r+1,c-1) + matrix(data,r+1,c+1);
gy = -matrix(data,r-1,c-1) - 2*matrix(data,r-1,c)
- matrix(data,r-1,c+1) +
matrix(data,r+1,c-1) + 2*matrix(data,r+1,c)
+ matrix(data,r+1,c+1);
matrix(output,r,c) = sqrt((float)(gx)*(float)(gx) +
(float)(gy)*(float)(gy));
}
}
}
这个C函数通过双重循环遍历图像的每个像素,计算其在x和y方向的边缘度量,并计算边缘度量的幅值。在1024x1024图像的测试中,该代码每秒可以计算161.5个Sobel幅值图像;使用1000个不同图像进行测试时,每秒可以计算158个图像。可以看出,该代码的性能主要受数学计算的限制,而不是内存带宽。
使用SSE指令的Sobel滤波器实现
使用SSE指令可以显著提高Sobel滤波器的性能。SSE指令可以同时处理多个数据,从而加快计算速度。以下是使用SSE指令的汇编代码:
%macro multipush 1-*
%rep %0
push %1
%rotate 1
%endrep
%endmacro
%macro multipop 1-*
%rep %0
%rotate -1
pop %1
%endrep
%endmacro
segment .text
global sobel, main
sobel:
.cols equ 0
.rows equ 8
.output equ 16
.input equ 24
.bpir equ 32
.bpor equ 40
multipush rbx, rbp, r12, r13, r14, r15
sub rsp, 48
cmp rdx, 3
jl .noworktodo
cmp rcx, 3
jl .noworktodo
mov [rsp+.input], rdi
mov [rsp+.output], rsi
mov [rsp+.rows], rdx
mov [rsp+.cols], rcx
mov [rsp+.bpir], rcx
imul rcx, 4
mov [rsp+.bpor], rcx
mov rax, [rsp+.rows];
mov rdx, [rsp+.cols]
sub rax, 2
mov r8, [rsp+.input]
add r8, rdx
mov r9, r8
mov r10, r8
sub r8, rdx
add r10, rdx
pxor xmm13, xmm13
pxor xmm14, xmm14
pxor xmm15, xmm15
.more_rows:
mov rbx, 1
.more_cols:
movdqu xmm0, [r8+rbx-1]
movdqu xmm1, xmm0
movdqu xmm2, xmm0
pxor xmm9, xmm9
pxor xmm10, xmm10
pxor xmm11, xmm11
pxor xmm12, xmm12
psrldq xmm1, 1
psrldq xmm2, 2
movdqa xmm3, xmm0
movdqa xmm4, xmm1
movdqa xmm5, xmm2
punpcklbw xmm3, xmm13
punpcklbw xmm4, xmm14
punpcklbw xmm5, xmm15
psubw xmm11, xmm3
psubw xmm9, xmm3
paddw xmm11, xmm5
psubw xmm9, xmm4
psubw xmm9, xmm4
psubw xmm9, xmm5
punpckhbw xmm0, xmm13
punpckhbw xmm1, xmm14
punpckhbw xmm2, xmm15
psubw xmm12, xmm0
psubw xmm10, xmm0
paddw xmm12, xmm2
psubw xmm10, xmm1
psubw xmm10, xmm1
psubw xmm10, xmm2
movdqu xmm0, [r9+rbx-1]
movdqu xmm2, xmm0
psrldq xmm2, 2
movdqa xmm3, xmm0
movdqa xmm5, xmm2
punpcklbw xmm3, xmm13
punpcklbw xmm5, xmm15
psubw xmm11, xmm3
psubw xmm11, xmm3
paddw xmm11, xmm5
paddw xmm11, xmm5
punpckhbw xmm0, xmm13
punpckhbw xmm2, xmm15
psubw xmm12, xmm0
psubw xmm12, xmm0
paddw xmm12, xmm2
paddw xmm12, xmm2
movdqu xmm0, [r10+rbx-1]
movdqu xmm1, xmm0
movdqu xmm2, xmm0
psrldq xmm1, 1
psrldq xmm2, 2
movdqa xmm3, xmm0
movdqa xmm4, xmm1
movdqa xmm5, xmm2
punpcklbw xmm3, xmm13
punpcklbw xmm4, xmm14
punpcklbw xmm5, xmm15
psubw xmm11, xmm3
paddw xmm9, xmm3
paddw xmm11, xmm5
paddw xmm9, xmm4
paddw xmm9, xmm4
paddw xmm9, xmm5
punpckhbw xmm0, xmm13
punpckhbw xmm1, xmm14
punpckhbw xmm2, xmm15
psubw xmm12, xmm0
paddw xmm10, xmm0
paddw xmm12, xmm2
paddw xmm10, xmm1
paddw xmm10, xmm1
paddw xmm10, xmm2
pmullw xmm9, xmm9
pmullw xmm10, xmm10
pmullw xmm11, xmm11
pmullw xmm12, xmm12
paddw xmm9, xmm11
paddw xmm10, xmm12
movdqa xmm1, xmm9
movdqa xmm3, xmm10
punpcklwd xmm9, xmm13
punpckhwd xmm1, xmm13
punpcklwd xmm10, xmm13
punpckhwd xmm3, xmm13
cvtdq2ps xmm0, xmm9
cvtdq2ps xmm1, xmm1
cvtdq2ps xmm2, xmm10
cvtdq2ps xmm3, xmm3
sqrtps xmm0, xmm0
sqrtps xmm1, xmm1
sqrtps xmm2, xmm2
sqrtps xmm3, xmm3
movups [rsi+rbx*4], xmm0
movups [rsi+rbx*4+16], xmm1
movups [rsi+rbx*4+32], xmm2
movlps [rsi+rbx*4+48], xmm3
add rbx, 14
cmp rbx, rdx
jl .more_cols
add r8, rdx
add r9, rdx
add r10, rdx
add rsi, [rsp+.bpor]
sub rax, 1
cmp rax, 0
jg .more_rows
.noworktodo:
add rsp, 48
multipop rbx, rbp, r12, r13, r14, r15
ret
这个汇编代码使用SSE指令同时处理多个像素,大大提高了计算效率。在相同的Core i7计算机上进行测试,该代码每秒可以计算1063个Sobel幅值图像;使用1000个不同图像进行测试时,每秒可以计算980个图像,比C版本快约6.2倍。
总结与展望
通过本文的介绍,我们了解了多种高性能编程的技巧和算法优化方法,包括循环优化、数组中计数位的算法优化以及图像边缘检测的Sobel滤波器优化。这些方法可以帮助我们在不同的场景下提高程序的性能。
在实际应用中,我们需要根据具体的需求和场景选择合适的优化方法。同时,还可以结合多种优化方法,进一步提高程序的性能。例如,在处理大规模数据时,可以同时使用循环优化和专门指令,以达到更好的效果。
未来,随着计算机硬件的不断发展,新的指令集和技术将不断涌现,我们可以进一步探索如何利用这些新技术来优化程序性能。同时,随着人工智能和大数据的发展,对程序性能的要求也将越来越高,高性能编程将变得更加重要。
练习
- 给定一个由x、y和z分量定义的3D点数组,编写一个函数来计算点对之间的距离矩阵。
- 给定一个n x 4的浮点型二维数组M和一个4个浮点数的向量v,计算Mv。
- 将Sobel函数转换为一个对图像进行任意3 x 3矩阵卷积的函数。
- 编写一个汇编函数,将图像转换为行程编码图像。
-
编写一个函数,使用基于公式
Xn+1 = (aXn + c) mod m的4个独立交错序列生成伪随机数填充数组。对于所有4个序列,使用m = 32,a的值分别为1664525、22695477、1103515245和214013,c的值分别为1013904223、1、12345和2531011。
不同优化方法的性能对比总结
为了更直观地展示不同优化方法在不同场景下的性能差异,我们将前面提到的数组中计数位以及Sobel滤波器的不同实现的性能数据进行汇总,如下表所示:
| 场景 | 实现方法 | 执行时间(秒) |
| ---- | ---- | ---- |
| 数组中计数位 | C函数(未优化) | 14.63 |
| 数组中计数位 | C函数(优化为while循环) | 3.34 |
| 数组中计数位 | 汇编实现(循环展开) | 2.52 |
| 数组中计数位 | 预计算每个字节的位数(C函数) | 0.24 |
| 数组中计数位 | 使用
popcnt
指令(汇编) | 0.04 |
| Sobel滤波器 | C实现 | 1 / 161.5 ≈ 0.0062(每秒161.5个图像,换算为单个图像时间) |
| Sobel滤波器 | 使用SSE指令(汇编) | 1 / 1063 ≈ 0.00094(每秒1063个图像,换算为单个图像时间) |
从这个表格中可以清晰地看到,不同的优化方法带来的性能提升是非常显著的。例如在数组中计数位的场景下,使用
popcnt
指令比最初的C函数实现快了近366倍;在Sobel滤波器场景下,使用SSE指令的汇编实现比C实现快了约6.6倍。
优化方法的选择策略
在实际编程中,如何选择合适的优化方法是一个关键问题。以下是一些选择优化方法的策略:
1.
分析瓶颈
:首先需要确定程序的性能瓶颈所在。可以使用性能分析工具来找出程序中耗时最多的部分,例如循环、递归调用等。如果发现某个循环占用了大量时间,那么就可以考虑对该循环进行优化,如合并循环、拆分循环等。
2.
考虑数据规模
:数据规模对优化方法的选择有很大影响。当处理小规模数据时,一些简单的优化方法可能就足够了;而当处理大规模数据时,可能需要使用更复杂的优化方法,如专门指令、并行计算等。例如,在数组中计数位的问题中,如果数组规模较小,C函数实现可能就可以满足需求;但如果数组规模非常大,使用
popcnt
指令或预计算每个字节的位数的方法就会更有优势。
3.
权衡复杂度
:不同的优化方法可能会带来不同的代码复杂度。在选择优化方法时,需要权衡性能提升和代码复杂度之间的关系。一些优化方法,如使用专门指令,可能会使代码变得复杂,但能带来显著的性能提升;而一些简单的优化方法,如移动循环不变代码,虽然性能提升可能相对较小,但代码复杂度较低。
4.
结合多种方法
:在很多情况下,可以结合多种优化方法来进一步提高性能。例如,在处理大规模数据的循环时,可以同时使用循环展开和专门指令,以达到更好的效果。
优化过程的流程图
下面是一个优化程序性能的通用流程图:
graph TD;
A[开始] --> B[分析程序性能瓶颈];
B --> C{瓶颈类型};
C -- 循环 --> D[考虑循环优化方法(合并、拆分等)];
C -- 递归 --> E[考虑移除递归];
C -- 数据访问 --> F[考虑缓存优化(预计算等)];
C -- 计算密集 --> G[考虑使用专门指令];
D --> H[实现优化并测试性能];
E --> H;
F --> H;
G --> H;
H --> I{性能是否满足需求};
I -- 是 --> J[结束];
I -- 否 --> B;
总结
高性能编程是一个复杂而又充满挑战的领域,需要我们不断地学习和实践。通过本文的介绍,我们了解了多种循环优化技巧、数组中计数位的算法优化以及图像边缘检测的Sobel滤波器优化方法。在实际编程中,我们要根据具体的需求和场景,选择合适的优化方法,并结合多种优化方法来提高程序的性能。
同时,我们也要认识到,优化是一个持续的过程,需要不断地分析和改进。随着计算机硬件的不断发展,新的优化方法和技术也会不断涌现,我们要保持学习的热情,不断探索和应用新的优化方法,以适应不断变化的需求。
拓展思考
- 并行计算的应用 :在现代计算机中,多核处理器已经成为主流。如何将本文中的优化方法与并行计算相结合,进一步提高程序的性能?例如,在处理大规模数组时,可以使用多线程并行处理不同的部分。
- 跨平台优化 :不同的计算机平台可能具有不同的硬件特性和指令集。如何针对不同的平台进行优化,以确保程序在各种平台上都能获得良好的性能?
- 机器学习与高性能编程的结合 :随着机器学习的发展,对程序性能的要求也越来越高。如何将高性能编程的方法应用到机器学习算法中,提高机器学习模型的训练和推理速度?
练习答案思路(仅供参考)
1. 计算3D点数组的距离矩阵
#include <math.h>
// 假设3D点结构体定义如下
typedef struct {
float x;
float y;
float z;
} Point3D;
// 计算两点之间的距离
float distance(Point3D p1, Point3D p2) {
float dx = p1.x - p2.x;
float dy = p1.y - p2.y;
float dz = p1.z - p2.z;
return sqrt(dx * dx + dy * dy + dz * dz);
}
// 计算距离矩阵
void distance_matrix(Point3D *points, int size, float **matrix) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
matrix[i][j] = distance(points[i], points[j]);
}
}
}
2. 计算矩阵M和向量v的乘积Mv
// 计算Mv
void matrix_vector_multiply(float **M, float *v, float *result, int n) {
for (int i = 0; i < n; i++) {
result[i] = 0;
for (int j = 0; j < 4; j++) {
result[i] += M[i][j] * v[j];
}
}
}
3. 将Sobel函数转换为任意3x3矩阵卷积函数
#include <math.h>
#define matrix(a,b,c) a[(b)*(cols)+(c)]
// 任意3x3矩阵卷积
void convolution(unsigned char *data, float *output, long rows, long cols, float kernel[3][3]) {
int r, c;
int i, j;
for (r = 1; r < rows - 1; r++) {
for (c = 1; c < cols - 1; c++) {
float sum = 0;
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
sum += kernel[i + 1][j + 1] * matrix(data, r + i, c + j);
}
}
matrix(output, r, c) = sum;
}
}
}
4. 编写汇编函数将图像转换为行程编码图像
行程编码是一种简单的图像压缩方法,它将连续的相同像素值编码为一个计数值和该像素值。以下是一个简单的思路:
; 假设图像数据存储在内存中,宽度为width,高度为height
; 输出的行程编码数据存储在另一个内存区域
segment .text
global rle_encode
rle_encode:
; 初始化寄存器等操作
xor rax, rax ; 用于计数
xor rbx, rbx ; 用于存储当前像素值
mov rcx, 0 ; 用于遍历图像
mov rdx, 0 ; 用于存储输出位置
.loop:
mov bl, [rdi + rcx] ; 读取当前像素值
mov rax, 1 ; 初始化计数为1
inc rcx ; 移动到下一个像素
.check_next:
cmp [rdi + rcx], bl ; 检查下一个像素是否相同
jne .store_rle ; 不同则存储行程编码
inc rax ; 相同则增加计数
inc rcx ; 移动到下一个像素
cmp rcx, rsi ; 检查是否到达图像末尾
jl .check_next ; 未到达则继续检查
.store_rle:
mov [rsi + rdx], al ; 存储计数
inc rdx
mov [rsi + rdx], bl ; 存储像素值
inc rdx
cmp rcx, rsi ; 检查是否到达图像末尾
jl .loop ; 未到达则继续循环
ret
5. 编写函数使用4个独立交错序列生成伪随机数填充数组
#include <stdio.h>
#define m 32
#define SIZE 100 // 数组大小
// 4个序列的参数
int a[4] = {1664525, 22695477, 1103515245, 214013};
int c[4] = {1013904223, 1, 12345, 2531011};
// 生成伪随机数填充数组
void fill_array(int *array) {
int x[4] = {1, 2, 3, 4}; // 初始值
for (int i = 0; i < SIZE; i++) {
int seq = i % 4;
x[seq] = (a[seq] * x[seq] + c[seq]) % m;
array[i] = x[seq];
}
}
以上练习答案只是一种可能的实现方式,实际应用中可能需要根据具体需求进行调整和优化。希望这些内容能帮助你更好地理解和应用高性能编程的相关知识。
超级会员免费看
173万+

被折叠的 条评论
为什么被折叠?



