첫째로, 필자는 내 문제를 해결하기 위해 아래 코드의 기능을 이해해야한다고 생각하지 않는다고 말하면서 접두사를 붙입니다. 이것은 주로 최적화 문제입니다. 이 코드는 수행중인 작업을 이해하는 것입니다.컴파일러가 루프 외부에서 레지스터를 설정하도록 유도
I있다 (동작) 다음 다소 최적화 컨벌루션 메인 루프 :
for(int i=0; i<length-kernel_length; i+=4){
acc = _mm_setzero_ps();
for(int k=0; k<KERNEL_LENGTH; k+=4){
int data_offset = i + k;
for (int l = 0; l < 4; l++){
data_block = _mm_load_ps(in_aligned[l] + data_offset);
prod = _mm_mul_ps(kernel_reverse[k+l], data_block);
acc = _mm_add_ps(acc, prod);
}
}
_mm_storeu_ps(out+i, acc);
}
KERNEL_LENGTH
는 각각, 제 in_aligned
입력 어레이 (이 때 콘볼 루션을 수행한다) 반복 4 배이다 반복은 한 샘플을 다른 샘플에서 왼쪽으로 이동합니다. 16 바이트로 정렬 된 위치에서 모든 샘플을 찾을 수 있습니다.
float kernel_block[4] __attribute__ ((aligned (16)));
__m128 kernel_reverse[KERNEL_LENGTH] __attribute__ ((aligned (16)));
// Repeat the kernel across the vector
for(int i=0; i<KERNEL_LENGTH; i++){
kernel_block[0] = kernel[kernel_length - i - 1];
kernel_block[1] = kernel[kernel_length - i - 1];
kernel_block[2] = kernel[kernel_length - i - 1];
kernel_block[3] = kernel[kernel_length - i - 1];
kernel_reverse[i] = _mm_load_ps(kernel_block);
}
코드는 정확하고 꽤 빨리 너무 알고리즘을 계산 : kernel_reverse
모든 샘플 4 벡터를 채우기 위해 4 번 반복하고 선언으로 정의와, 반전 된 커널입니다. 루프 아래의 기계 코드로 컴파일 :
나는 내 질문은 이것이다 gcc -std=c99 -Wall -O3 -msse3 -mtune=core2
와 코드를 컴파일합니다. 이 루프 안에는 커널을 매번로드하는 데별로 중요하지 않은 명령어가 필요합니다. 커널은 루프 반복마다 변경되지 않으므로 원칙적으로 SSE 레지스터에 보관할 수 있습니다. 내가 알기에, 커널을 쉽게 저장할 수있는 충분한 레지스터가있다. (실제로, 기계 코드는 너무 많은 레지스터 압박을 제시하지 않는다.)
모든 루프에서 커널을로드하지 않도록 컴파일러를 설득하려면 어떻게합니까?
커널 길이를 일정하게 설정하면 컴파일러에서 자동으로이 작업을 수행 할 것으로 예상했습니다.
testl %edx, %edx
jle .L79
leaq (%rcx,%rcx,2), %rsi
movaps -144(%rbp), %xmm6
xorps %xmm2, %xmm2
leal -1(%rdx), %ecx
movaps -128(%rbp), %xmm5
xorl %eax, %eax
movaps -112(%rbp), %xmm4
leaq 0(%r13,%rsi,4), %rsi
shrl $2, %ecx
addq $1, %rcx
movaps -96(%rbp), %xmm3
salq $4, %rcx
.p2align 4,,10
.p2align 3
.L80:
movaps 0(%r13,%rax), %xmm0
movaps (%r14,%rax), %xmm1
mulps %xmm6, %xmm0
mulps %xmm5, %xmm1
addps %xmm2, %xmm0
addps %xmm1, %xmm0
movaps (%r9,%rax), %xmm1
mulps %xmm4, %xmm1
addps %xmm1, %xmm0
movaps (%rsi,%rax), %xmm1
mulps %xmm3, %xmm1
addps %xmm1, %xmm0
movups %xmm0, (%rbx,%rax)
addq $16, %rax
cmpq %rcx, %rax
jne .L80
.L79:
편집 : 전체 코드 목록은 다음과 같다 :
#define KERNEL_LENGTH 4
int convolve_sse_in_aligned_fixed_kernel(float* in, float* out, int length,
float* kernel, int kernel_length)
{
float kernel_block[4] __attribute__ ((aligned (16)));
float in_aligned[4][length] __attribute__ ((aligned (16)));
__m128 kernel_reverse[KERNEL_LENGTH] __attribute__ ((aligned (16)));
__m128 data_block __attribute__ ((aligned (16)));
__m128 prod __attribute__ ((aligned (16)));
__m128 acc __attribute__ ((aligned (16)));
// Repeat the kernel across the vector
for(int i=0; i<KERNEL_LENGTH; i++){
int index = kernel_length - i - 1;
kernel_block[0] = kernel[index];
kernel_block[1] = kernel[index];
kernel_block[2] = kernel[index];
kernel_block[3] = kernel[index];
kernel_reverse[i] = _mm_load_ps(kernel_block);
}
/* Create a set of 4 aligned arrays
* Each array is offset by one sample from the one before
*/
for(int i=0; i<4; i++){
memcpy(in_aligned[i], (in+i), (length-i)*sizeof(float));
}
for(int i=0; i<length-kernel_length; i+=4){
acc = _mm_setzero_ps();
for(int k=0; k<KERNEL_LENGTH; k+=4){
int data_offset = i + k;
for (int l = 0; l < 4; l++){
data_block = _mm_load_ps(in_aligned[l] + data_offset);
prod = _mm_mul_ps(kernel_reverse[k+l], data_block);
acc = _mm_add_ps(acc, prod);
}
}
_mm_storeu_ps(out+i, acc);
}
// Need to do the last value as a special case
int i = length - kernel_length;
out[i] = 0.0;
for(int k=0; k<kernel_length; k++){
out[i] += in_aligned[0][i+k] * kernel[kernel_length - k - 1];
}
return 0;
}
'gcc -S'의 출력물이 객체 파일을 분해하는 것보다 약간 더 읽기 쉽다는 것을 알았습니다. –
'i'가 상수가 아니기 때문에 배열 인덱스를 미리 계산하고, 루프의 시작 부분에 int index = kernel_length - i - 1;을 설정하여 실험하고,'kernel_block [0] = kernel [색인]; 등등. – Lundin
@PascalCuoq 의견에 따르면, 나는'gcc -S'의 결과물을 사용하는 것으로 바꿨다. 이것은 내가 제안하는 루프를 _ 내 _ 내게 제안합니다. 나는이 주장을 확인하는 누군가의 온전함에 정말로 감사 할 것입니다. –