kldp 를 눈팅하다가 오늘 재밌는 문서를 읽게 되었습니다. 2001년도에 AMD 에서 나온 문서였는데, 그 내용이 상당히 흥미로워서 몇 가지 내용을 옮겨볼까 합니다.
메모리에서 연속된 값들을 다른 곳으로 복사하기 위해 사용할 수 있는 가장 간단한 어셈블리 코드는 다음과 같습니다.
Assembly (x86)
1
2
3
4
5
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
shlecx,3//converttobytecount
repmovsb
rep movsb 명령은 repeat move single byte (바이트 단위로 값들을 반복해서 옮긴다.)라는 의미를 가집니다. esi 가 가리키는 곳에 있는 값을 edi 로 ecx 에 있는 개수 만큼 복사하게 됩니다. 이렇게 할 경우 일초에 약 620MB 를 복사할 수 있다고 하네요.
이 코드를 byte 단위가 아니라 4 byte 단위로 반복해서 복사를 하도록 하면 어떻게 될까요? 우선 코드는 다음과 같이 변하겠네요.
Assembly (x86)
1
2
3
4
5
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
shlecx,1//converttoDWORDcount
repmovsd
shl 인스트럭션은 두개의 오퍼랜드를 가지며 (shl operand1 operand2) operand1 에 있는 값을 operand2 에 있는 값만큼 왼쪽으로 shift 를 시키게 됩니다. movsd 는 mov single dword[1] 라고 보시면 됩니다.
결과적으로 이렇게 코드를 수정함으로 인해 1초에 640MB 를 복사할 수 있게 됩니다. 3% 정도 성능 향상이 생기네요.
그런데 최근에 나온 프로세서들에서는 rep 같은 복잡한 인스트럭션을 내부적으로 RISC 명령으로 바꿔서 실행하다 보니, 그리 효율적이지 못하답니다. 그러므로 rep 를 사용하지 말고 반복문을 사용해보도록 합시다.
Assembly (x86)
1
2
3
4
5
6
7
8
9
10
11
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
shlecx,1//converttoDWORDcount
copyloop:
moveax,dwordptr[esi]
movdwordptr[edi],eax
addesi,4
addedi,4
dececx
jnzcopyloop
코드가 뭔가 좀 길어졌죠? 위 코드를 c로 표현하면 아래와 같습니다.
C
1
2
3
4
5
int*src=esi;
int*dst=edi;
inti;
for(i=len;i!=0;i--){
dst++=src++;
c로 표현하니 어디서 많이 쓰던 코드죠? 쨌든! 이렇게 하니 1초에 650MB 를 복사할수 있었고 결과적으로 1.5% 정도 성능이 향상되었답니다.
그럼 여기다가 Loop 코드를 최적화 하는 기법인 Loop Unrolling 을 적용해봅시다. [2]
Assembly (x86)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
shrecx,1//convertto16-bytesizecount
//(assumeslen/16isaninteger)
copyloop:
moveax,dwordptr[esi]
movdwordptr[edi],eax
movebx,dwordptr[esi+4]
movdwordptr[edi+4],ebx
moveax,dwordptr[esi+8]
movdwordptr[edi+8],eax
movebx,dwordptr[esi+12]
movdwordptr[edi+12],ebx
addesi,16
addedi,16
dececx
jnzcopyloop
자 룹을 펼쳤더니 1초에 640MB 를 복사하였고, 결과적으로 1.5% 만큼 성능이 떨어졌습니다. 하지만 다행히도 Loop Unrolling 을 적용하고 나니 최적화를 할 여지가 많아졌네요.
캐쉬를 좀 더 잘 활용할 수 있도록 코드 순서를 바꿔봅시다.
Assembly (x86)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
shrecx,1//convertto16-bytesizecount
copyloop:
moveax,dwordptr[esi]
movebx,dwordptr[esi+4]
movdwordptr[edi],eax
movdwordptr[edi+4],ebx
moveax,dwordptr[esi+8]
movebx,dwordptr[esi+12]
movdwordptr[edi+8],eax
movdwordptr[edi+12],ebx
addesi,16
addedi,16
dececx
jnzcopyloop
이젠 1초에 660MB 를 복사할 수 있게 되었고, 3% 만큼 성능 향상이 일어났습니다.
여기서 끝이 아닙니다. 첨에 movsb 대신 movd 를 사용해서 1byte씩이 아닌 4byte 씩 복사를 하는 방법을 통해 최적화를 진행했었는데요, MMX 를 사용할 경우 movq 등의 인스트럭션을 이용해서 한 번에 8byte 씩을 복사하는게 가능해집니다. 또한 mm0~7 이라는 특수한 레지스터를 활용할 수 있으니 8*8 = 64 즉 한번에 64byte 씩을 복사해봅시다.
Assembly (x86)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
leaesi,[esi+ecx*8]//endofsource
leaedi,[edi+ecx*8]//endofdestination
negecx//useanegativeoffset
copyloop:
movqmm0,qwordptr[esi+ecx*8]
movqmm1,qwordptr[esi+ecx*8+8]
movqmm2,qwordptr[esi+ecx*8+16]
movqmm3,qwordptr[esi+ecx*8+24]
movqmm4,qwordptr[esi+ecx*8+32]
movqmm5,qwordptr[esi+ecx*8+40]
movqmm6,qwordptr[esi+ecx*8+48]
movqmm7,qwordptr[esi+ecx*8+56]
movqqwordptr[edi+ecx*8],mm0
movqqwordptr[edi+ecx*8+8],mm1
movqqwordptr[edi+ecx*8+16],mm2
movqqwordptr[edi+ecx*8+24],mm3
movqqwordptr[edi+ecx*8+32],mm4
movqqwordptr[edi+ecx*8+40],mm5
movqqwordptr[edi+ecx*8+48],mm6
movqqwordptr[edi+ecx*8+56],mm7
addecx,8
jnzcopyloop
emms//emptytheMMXstate
MMX 용 레지스터들인 mm0~7 은 FPU stack 의 일부를 활용하게 되므로, 이 레지스터 값을 바꿔주게 될 경우 FPU 와 관련해서 문제를 일으킬 수 있습니다. EMMX 는 이를 방지하기 위해 사용해야 하는 인스트럭션이 되겠습니다. 하여튼 이렇게 바꾸니 1초에 705MB 를 복사할 수 있게 되었고, 7% 만큼 성능이 향상되었습니다.
이젠 movntq 라는 인스트럭션을 통해 cache 를 우회해서 writing 을 진행해봅시다.
Assembly (x86)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
leaesi,[esi+ecx*8]
leaedi,[edi+ecx*8]
negecx
copyloop:
movqmm0,qwordptr[esi+ecx*8]
movqmm1,qwordptr[esi+ecx*8+8]
movqmm2,qwordptr[esi+ecx*8+16]
movqmm3,qwordptr[esi+ecx*8+24]
movqmm4,qwordptr[esi+ecx*8+32]
movqmm5,qwordptr[esi+ecx*8+40]
movqmm6,qwordptr[esi+ecx*8+48]
movqmm7,qwordptr[esi+ecx*8+56]
movntqqwordptr[edi+ecx*8],mm0
movntqqwordptr[edi+ecx*8+8],mm1
movntqqwordptr[edi+ecx*8+16],mm2
movntqqwordptr[edi+ecx*8+24],mm3
movntqqwordptr[edi+ecx*8+32],mm4
movntqqwordptr[edi+ecx*8+40],mm5
movntqqwordptr[edi+ecx*8+48],mm6
movntqqwordptr[edi+ecx*8+56],mm7
addecx,8
jnzcopyloop
sfence
emms
movntq 를 활용한 다음에는 write buffer 를 비워주기 위해 sfence 를 사용해야 한다는군요. write 부분이 movq 에서 movntq 로 바뀌었고 emms 앞에 sfence 가 들어간 것을 제외하면 코드는 동일합니다. 하지만 성능 향상은 60% 로 굉장하네요. 1초에 1130MB 를 복사할 수 있었다고 합니다.
이젠 prefetch 도 활용해봅시다.
Assembly (x86)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
movesi,[src]//sourcearray
movedi,[dst]//destinationarray
movecx,[len]//numberofQWORDS(8bytes)
leaesi,[esi+ecx*8]
leaedi,[edi+ecx*8]
negecx
copyloop:
prefetchnta[esi+ecx*8+512]//fetchaheadby512bytes
movqmm0,qwordptr[esi+ecx*8]
movqmm1,qwordptr[esi+ecx*8+8]
movqmm2,qwordptr[esi+ecx*8+16]
movqmm3,qwordptr[esi+ecx*8+24]
movqmm4,qwordptr[esi+ecx*8+32]
movqmm5,qwordptr[esi+ecx*8+40]
movqmm6,qwordptr[esi+ecx*8+48]
movqmm7,qwordptr[esi+ecx*8+56]
movntqqwordptr[edi+ecx*8],mm0
movntqqwordptr[edi+ecx*8+8],mm1
movntqqwordptr[edi+ecx*8+16],mm2
movntqqwordptr[edi+ecx*8+24],mm3
movntqqwordptr[edi+ecx*8+32],mm4
movntqqwordptr[edi+ecx*8+40],mm5
movntqqwordptr[edi+ecx*8+48],mm6
movntqqwordptr[edi+ecx*8+56],mm7
addecx,8
jnzcopyloop
sfence
emms
위 코드에는 현재 복사할 차례의 512 바이트를 미리 읽어두라는 의미의 prefetchnta 인스트럭션이 추가되었습니다. 한 번에 복사하는건 64Byte 인데 왜 512Byte 를 읽으라고 했는지 살짝 의문이네요. 제 생각에는 문서를 작성하신 분이 버그를 낸거라고 생각합니다.
하여튼 이젠 1초에 1240MB 를 복사할 수 있게 되었고, 10% 만큼 더 성능 향상이 생겼네요.
지금까지의 방법만으로도 꽤 많은 성능 향상이 있었지만, 한 번에 한 캐쉬 라인[3]만을 활용하고 있습니다. 하지만 실제 CPU 에는 훨씬 많은 캐쉬 라인이 존재하므로 이를 더 잘 활용할 수 있도록 코드를 수정해보겠습니다.
여기서는 cache 가 1024 개의 cache line 을 가지고 있다고 가정했네요. (16진수인 400h 는 10진수로 바꿀 경어 1024 가 됩니다.) 미리 캐쉬 사이즈만큼 prefetch 명령들을 내려놓은 뒤 값들을 복사하게 될 때 쯤이면 이미 값들이 캐쉬에 올라와있게 되니 딜레이를 줄일 수 있게 되겠습니다.
효과가 있을까 싶지만, 실제 1초에 1976MB 를 복사할 수 있었고, 59% 의 성능향상이 추가로 발생했습니다. 초기 코드에 비하면 300% 의 성능 향상이라네요. 신기하죠. 😉
전 상당히 흥미롭게 읽었었는데, (비슷한 걸 해본 경험도 있고 해서) 재밌었는지 모르겠네요. sfence 나 movqnta, prefetchnta 같은 캐쉬와 관련된 명령들은 정확히 무슨 용도인지 이해하지 못하고 있었는데 이 문서를 통해 이해할 수 있었던 것 같네요. 관심이 있으신 분은 아래 문서를 읽어보시면 되겠습니다. FPU 관련된 최적화도 다루고 있는데, 관심이 없어서 그 부분은 옮기질 않았습니다. 그럼 다들 즐거운 주말 보내시길 😉
[1] x86 호환 아키텍쳐에서는 사이즈를 byte, word, dword, qword, dqword 식으로 표현합니다. 이는 각각 1, 2, 4, 8, 16 바이트를 의미하며, word 가 2바이트이고 나머지의 앞에 붙은 알파벳들은 각각 double(*2), quad(*4), double quad(*8)를 나타내는 것이죠.
[2] Loop Unrolling 은 Loop 을 펼쳐서 파이프라인의 덕을 더 많이 볼 수 있도록 코드를 수정하는 방법입니다. 예를 들어
C
1
2
for(i=0;i<16;i++)
dst[i]=src[i];
위와 같은 코드를
C
1
2
3
4
5
6
for(i=0;i<16;i+=4){
dst[i]=src[i];
dst[i+1]=src[i+1];
dst[i+2]=src[i+2];
dst[i+3]=src[i+3];
}
이렇게 바꿀 경우 branch 로 인한 pipeline hazard 도 줄일 수 있고, add instruction 도 1/4 만큼만 사용하게 됨으로 인해 성능이 향상될 여지가 많습니다.
[3] 메모리에 어떤 값을 읽어들일 때 CPU 에서는 바이트 단위로 값을 읽지 않고, block 단위로 값들을 cache 에 복사하는데, 이 block 의 크기를 cache line 이라고 표현합니다. 펜티엄 계열의 경우 대부분 64byte 입니다.
대강 생각을 해보니 정말 mmx 를 이용해서 빠르게 연산을 하려면 위와 같이 하는게 가장 빠르겠군요. 다만 레지스터를 많이 쓰고 완전히 asm 코딩을 해야한다는 게 조금 귀찮겠군요. 😉
위의 다이아그램에 있는 과정을 통해 4×4 matrix * 4×4 matrix 의 한 row 씩을 계산해낼 수 있습니다. 대강 계산했을 때 3배 이상의 속도 향상이 있을거라고 예상되던데 과연~
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio .h>
// A matrix
shorts1[16]={
1,2,3,4,
5,6,7,8,
9,10,11,12,
13,14,15,16,
};
// Transpose(B matrix)
shorts2[16]={
17,21,25,29,
18,22,26,30,
19,23,27,31,
20,24,28,32
};
// Destination matrix
shortd[16];
intj,i;
intmain(intargc,char**argv){
__asm__("movq (s1), %mm0");
__asm__("movq %mm0, %mm1");
__asm__("movq %mm0, %mm2");
__asm__("punpckhdq %mm2, %mm0");
__asm__("punpckldq %mm2, %mm1");
__asm__("movq %mm0, %mm6");
__asm__("movq %mm1, %mm7");
__asm__("movq (s2), %mm2");
__asm__("mov $1, %eax");
__asm__("movq s2(,%eax,8), %mm4");
__asm__("movq %mm2, %mm3");
__asm__("punpckhdq %mm4, %mm2");
__asm__("punpckldq %mm4, %mm3");
__asm__("pmaddwd %mm2, %mm0");
__asm__("pmaddwd %mm3, %mm1");
__asm__("paddw %mm1, %mm0");
__asm__("movq %mm6, %mm1");
__asm__("movq %mm7, %mm2");
__asm__("mov $2, %eax");
__asm__("movq s2(,%eax,8), %mm3");
__asm__("mov $3, %eax");
__asm__("movq s2(,%eax,8), %mm5");
__asm__("movq %mm3, %mm4");
__asm__("punpckhdq %mm5, %mm3");
__asm__("punpckldq %mm5, %mm4");
__asm__("pmaddwd %mm3, %mm1");
__asm__("pmaddwd %mm4, %mm2");
__asm__("paddw %mm2, %mm1");
__asm__("packssdw %mm1, %mm0");
__asm__("movq %mm0, (d)");
for(j=0;j<4;j++){
for(i=0;i<4;i++){
fprintf(stderr,"\t%3d",d[j*4+i]);
}
fprintf(stderr,"\n");
}
return0;
}
코드로 옮기니 위와 같군요. 중간에 실수로 바이트오더를 헷갈려서 연산 결과가 뒤집혔었습니다. 정상적인 결과는 250 260 270 280 이 나와야 하는데 280 270 260 250 이 나와버리더군요. 아아 이거 다시 하고 싶은 작업이 아니네요;
흐흣 그래도 오랫만에 어셈블리 관련된 것들을 생각하고 있는데, 이것도 가끔 하니까 재밌네요. 근데 길어지면 할만하지 않다는거 -_-!
p.s) 전체 연산 코드를 보고 싶으시면 http://mytears.org/resources/mysrc/c/mmx.c 를 보시길 😉
몇 일 전에 썼던 글에서 테스트를 해본 내용을 바탕으로 4×4 matrix multiply 연산을 mmx 를 이용해서 구현해봤습니다.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio .h>
// A matrix
shorts1[16]={
1,2,3,4,
5,6,7,8,
9,10,11,12,
13,14,15,16,
};
// Transpose(B matrix)
shorts2[16]={
17,21,25,29,
18,22,26,30,
19,23,27,31,
20,24,28,32
};
// Destination matrix
shortd[16];
shortt[4];
inti,j;
longstart,end;
intmain(intargc,char**argv){
intk;
for(j=0;j<4;j++){
for(i=0;i<4;i++){
d[j*4+i]=0;
for(k=0;k<4;k++){
d[j*4+i]+=s1[j*4+k]*s2[i*4+k];
}
}
}
fprintf(stderr,"c version\n\n");
for(j=0;j<4;j++){
for(i=0;i<4;i++){
fprintf(stderr,"\t%3d",d[j*4+i]);
}
fprintf(stderr,"\n");
}
return0;
}
위와 같은 c version 의 코드를 작성한 후 아래와 같은 asm version 으로 컨버팅을 해봤는데, 100000 번 반복해서 연산을 하도록 해본 결과 mmx 버젼이 c 버젼보다 3배 정도 빠르게 연산을 하는 것을 확인할 수 있었습니다. (-O0 옵션과 함께 컴파일 했을 경우)
하지만 -O3 옵션과 함께 컴파일하게 되면 asm 버젼은 무한룹에 빠진 듯한 모습을 보여줬고, c 버젼의 수행속도가 -O0 로 컴파일한 asm 버젼보다 빠른 현상이 발생했습니다. 이유는 알 수 없음 -_-;
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <asm /mmx.h>
// A matrix
shorts1[16]={
1,2,3,4,
5,6,7,8,
9,10,11,12,
13,14,15,16,
};
// Transpose(B matrix)
shorts2[16]={
17,21,25,29,
18,22,26,30,
19,23,27,31,
20,24,28,32
};
// Destination matrix
shortd[16];
shortt[4];
inti,j;
intmain(intargc,char**argv){
intloop;
for(loop=0;loop<10000;loop++){
for(j=0;j<4;j++){
for(i=0;i<4;i++){
__asm__("mov j, %eax");
__asm__("movq s1(,%eax,8), %mm0");
__asm__("mov i, %eax");
__asm__("movq s2(,%eax,8), %mm1");
__asm__("pmullw %mm1, %mm0");
__asm__("movq %mm0, (t)");
d[j*4+i]=t[0]+t[1]+t[2]+t[3];
}
}
}
for(j=0;j<4;j++){
for(i=0;i<4;i++){
fprintf(stderr,"\t%3d",d[j*4+i]);
}
fprintf(stderr,"\n");
}
return0;
}
8×8 matrix 는 뭔가 좀 더 생각해야할 것 같으니 나중에 정말 필요한 일 있을 때 구현을 해봐야겠습니다. -_-;
inline asm 작업을 하면서 eax 레지스터 값을 백업하지 않고 저렇게 사용해도 되는지는 잘 모르겠지만 –;; 하여튼 저 코드에 한해서는 별 문제 없으니 패스~ 꺄홋!!
요새 matrix 연산을 이용한 프로그램 조각 몇 가지를 짜보고 있는데, mmx 같은 SIMD instruction 을 사용하면 matrix 연산의 속도를 확 올릴 수 있지 않을까 싶은 생각이 들길래 inline asm 을 이용해서 간단한 mmx 코드를 만들어보았습니다.
위와 같은 코드를 작성하고, gcc mmx.c 를 통해 컴파일해서 돌려보니 간단히 성공 -_-v
c 코드를 사용할 경우 s1[0] load, s2[0] load, multiply, save to d[0] 와 같은 인스트럭션을 네 번 반복해서 실행하는 반면 mmx 를 사용할 경우 movq 를 통해 연속된 WORD 네 개를 mmx register 로 복사하고, pmullw 를 이용 4 개의 값을 한 인스트럭션에 연산을 하는 것을 통해 속도를 확 끌어올릴 수 있는거죠. 😉
다만 헷갈리는게 인텔의 메뉴얼에 나와있는 인자 순서와, AT&T 방식이 달라서 좀 헷갈리는군요.
Intel: movq mm0, [s1]
AT&T: movq (s1), %mm0
Intel 메뉴얼에서 설명하는 바에 의하면 첫번째 operland 가 destination 이 되고, 두번째 operland 가 destination 이 되는 반면 AT&T 방식에서는 거꾸로 첫번째 operland 가 src, 두번째 operland 가 dst 가 됩니다.
또한 주소값을 넘겨줄 때 intel 방식은 [] 로 감싸주면 되지만, AT&T 에서는 () 로 감싸줘야하고, 레지스터 이름 앞에 %를 붙여줘야 하는 규칙도 있어서 뭔가 대빵 귀찮네요. -_-;
참고로 gcc 에서 -masm=intel 옵션을 사용하면 intel 방식으로 어셈블리 명령어를 작성하는 것도 가능합니다.
p.s) movq 는 4개의 WORD 를 mmx register 로 복사하는 명령인데 –;; mm0 ~ mm7 식으로 64bit register name 을 써줘야 하는데 xmm0~xmm7 같은 sse 용 register 이름을 쓰는 바람에 잘못된 인스트럭션 사용이라고 계속 에러가나서 한참 헤맸네요;
여기저기 돌아다니다 보니 멋진 플러그인들이 많길래 이것저것 추가해보았습니다.
1. Korean Trackback
이글루스에서 오는 트랙백이 euc-kr 로 인코딩되어 있기 때문에, utf-8 기반의 워드프레스에선 이글루스에서 보내는 트랙백을 제대로 받을 수 없기에… 직접 플러그인을 작성해서 추가해줬습니다. -_-v
url: http://b.mytears.org/2006/09/396
2. iG:Syntax Hiliter
혹시나 포스트에 프로그램 코드를 삽입할 일이 있을 경우를 대비해서, 코드 하일라이팅을 위한 플러그인을 추가했습니다. 상당히 많은 언어를 지원합니다만 sh (쉘스크립트) 는 지원하지 않아서 약간 아쉽네요. 사용 예는 아래와 같습니다.
C
1
2
3
4
5
#include <stdio .h>
intmain(intargc,char**argv){
fprintf(stderr,"%s\n","Hello World");
return0;
}
url: http://blog.igeek.info/wp-plugins/igsyntax-hiliter/
3. wp-scripts, ajax-spoiler
wp-scripts 는 prototype.js 등을 헤더에 삽입해주는 역할을 하고, ajax-spoiler 는 tt 에서와 같이 텍스트를 숨겼다가 보여줬다 하는 기능을 사용할 수 있도록 해줍니다.
tt 처럼 그냥 단순히 보였다 감췄다 정도가 아니라 애니메이션 효과까지 줄 수 있어서 상당히 멋드러집니다. 😉
url: http://082net.com/tag/wp-scripts/
url: http://082net.com/tag/aj-spoiler/
4. bad behavior
request 를 분석해서 봇이라고 생각되면 차단합니다. 적용 후 확실히 스팸이 줄었습니다. (하루 120 통 쯤에서 10통 이하.. 그나마 akismet 에 나머지는 걸립니다.)
url: http://www.homelandstupidity.us/software/bad-behavior/
조금만 부지런하면 이래저래 편리해지는 아름다운 워드프레스 세상입니다. 혹시 또 멋진 플러그인들을 알고 계신 분들은 트랙백 부탁드리겠습니다. 😉