在很多的網絡開發中,經常會碰到一些內存轉換,如下面的場景:
[cpp]
#define PACKAGE_PARSE_ERROR -1
#define PACKAGE_PARSE_OK 0
int parse_package( int* a, int* b, int* c, int* d, char* buf, int buf_len )
{
if( !buf || buf_len < 16 ){
return PACKAGE_PARSE_ERROR;
}
memcpy( a, buf, 4 );
memcpy( b, buf + 4, 4 );
memcpy( c, buf + 8, 4 );
memcpy( d, buf + 12, 4 );
return PACKAGE_PARSE_OK;
}
#define PACKAGE_PARSE_ERROR -1
#define PACKAGE_PARSE_OK 0
int parse_package( int* a, int* b, int* c, int* d, char* buf, int buf_len )
{
if( !buf || buf_len < 16 ){
return PACKAGE_PARSE_ERROR;
}
memcpy( a, buf, 4 );
memcpy( b, buf + 4, 4 );
memcpy( c, buf + 8, 4 );
memcpy( d, buf + 12, 4 );
return PACKAGE_PARSE_OK;
}
這是網絡解包的過程中的一個調用,封包的過程則是逆過程。
像這樣的應用其實完全可以用整型強制轉換來代替,而且效率會至少提高一倍。
為了說明問題,我們舉個簡單的例子:
[cpp]
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
int main()
{
int s;
char buffer[4];
memcpy(&s, buffer, 4 );
s = *(int*)(buffer);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
int main()
{
int s;
char buffer[4];
memcpy(&s, buffer, 4 );
s = *(int*)(buffer);
return 0;
}
第10行和第11行的效果是一樣的,10行采用的是內存復制,11行采用的是強制轉換,為了方便比較,我們看一下匯編代碼:
[cpp]
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
leaq -16(%rbp), %rcx
leaq -4(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
leaq -16(%rbp), %rax
movl (%rax), %eax
movl %eax, -4(%rbp)
movl $0, %eax
leave
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
leaq -16(%rbp), %rcx
leaq -4(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
leaq -16(%rbp), %rax
movl (%rax), %eax
movl %eax, -4(%rbp)
movl $0, %eax
leave
代碼中可以看出,內存復制方法占用了7-12行,共6行,強制轉換占用了13-15行,共3行,指令上少了一半。
深究一下其實還不止,因為第12行其實是一個函數調用,必然會有棧的遷移,所以強制轉換的效率比內存復制起碼高一倍。
再看看glibc 的memcpy函數實現:
[cpp]
void *memcpy (void *dstpp, const void *srcpp, size_t len )
{
unsigned long int dstp = (long int) dstpp;
unsigned long int srcp = (long int) srcpp;
if (len >= OP_T_THRES)
{
len -= (-dstp) % OPSIZ;
BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
WORD_COPY_FWD (dstp, srcp, len, len);
}
BYTE_COPY_FWD (dstp, srcp, len);
return dstpp;
}
void *memcpy (void *dstpp, const void *srcpp, size_t len )
{
unsigned long int dstp = (long int) dstpp;
unsigned long int srcp = (long int) srcpp;
if (len >= OP_T_THRES)
{
len -= (-dstp) % OPSIZ;
BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
WORD_COPY_FWD (dstp, srcp, len, len);
}
BYTE_COPY_FWD (dstp, srcp, len);
return dstpp;
}
9-11行分別是三種處理方法,取決於 len 與 OP_T_THRES的比較,一般 OP_T_THRES 是8或16,對於len 小於OP_T_THRES的內存復制,glibc采用的是字節方式轉換,即遍歷每個字節,第個字節都要經過 “內存--寄存器--內存” 這個過程,CPU指令上可以說多了平空多了一倍。
從上面的分析可以看出,強制轉換是節省了很大的運算時間,效率上至少提高一倍。不要小看這樣的提升,在每秒幾萬並發的情況下,尤其每個並發都存在解包和封包的過程,這樣的處理可以給我們帶來相當大的性能提升。
開頭中提到的解包過程,我們可以巧秒地運用強制轉換,下面列出兩種方法:
[cpp]
int parse_package( int* a, int* b, int* c, int* d, char* buf, int buf_len )
{
if( !buf || buf_len < 16 ){
return PACKAGE_PARSE_ERROR;
}
memcpy( a, buf, 4 );
memcpy( b, buf + 4, 4 );
memcpy( c, buf + 8, 4 );
memcpy( d, buf + 12, 4 );
return PACKAGE_PARSE_OK;
}
int parse_package( int* a, int* b, int* c, int* d, char* buf, int buf_len )
{
if( !buf || buf_len < 16 ){
return PACKAGE_PARSE_ERROR;
}
memcpy( a, buf, 4 );
memcpy( b, buf + 4, 4 );
memcpy( c, buf + 8, 4 );
memcpy( d, buf + 12, 4 );
return PACKAGE_PARSE_OK;
}
[cpp]
int parse_package2( int* a, int* b, int* c, int* d, char* buf, int buf_len )
{
int* ibuf;
if( !buf || buf_len < 16 ){
return PACKAGE_PARSE_ERROR;
}
ibuf = buf;
*a = ibuf[0];
*b = ibuf[1];
*c = ibuf[2];
*d = ibuf[3];
return PACKAGE_PARSE_OK;
}
int parse_package2( int* a, int* b, int* c, int* d, char* buf, int buf_len )
{
int* ibuf;
if( !buf || buf_len < 16 ){
return PACKAGE_PARSE_ERROR;
}
ibuf = buf;
*a = ibuf[0];
*b = ibuf[1];
*c = ibuf[2];
*d = ibuf[3];
return PACKAGE_PARSE_OK;
}
parse_package匯編代碼:
[cpp]
parse_package:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rdx, -24(%rbp)
movq %rcx, -32(%rbp)
movq %r8, -40(%rbp)
movl %r9d, -44(%rbp)
cmpq $0, -40(%rbp)
je .L2
cmpl $15, -44(%rbp)
jg .L3
.L2:
movl $-1, %eax
jmp .L4.
L3:
movq -40(%rbp), %rcx
movq -8(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movq -40(%rbp), %rax
leaq 4(%rax), %rcx
movq -16(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movq -40(%rbp), %rax
leaq 8(%rax), %rcx
movq -24(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movq -40(%rbp), %rax
leaq 12(%rax), %rcx
movq -32(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movl $0, %eax
parse_package:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rdx, -24(%rbp)
movq %rcx, -32(%rbp)
movq %r8, -40(%rbp)
movl %r9d, -44(%rbp)
cmpq $0, -40(%rbp)
je .L2
cmpl $15, -44(%rbp)
jg .L3
.L2:
movl $-1, %eax
jmp .L4.
L3:
movq -40(%rbp), %rcx
movq -8(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movq -40(%rbp), %rax
leaq 4(%rax), %rcx
movq -16(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movq -40(%rbp), %rax
leaq 8(%rax), %rcx
movq -24(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movq -40(%rbp), %rax
leaq 12(%rax), %rcx
movq -32(%rbp), %rax
movl $4, %edx
movq %rcx, %rsi
movq %rax, %rdi
call memcpy
movl $0, %eax
L3段是我們的主段落,對a的賦值:
24-28行都是在“壓棧”,為了memcpy函數內取出來,加上29行一共是6條,memcpy 解棧指令數>=3, 去處指令數>=4,不加算返回指令,一共指令數>6+3+4=13。
parse_package2匯編代碼:
[cpp]
parse_package2:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq %rdx, -40(%rbp)
movq %rcx, -48(%rbp)
movq %r8, -56(%rbp)
movl %r9d, -60(%rbp)
cmpq $0, -56(%rbp)
je .L7
cmpl $15, -60(%rbp)
jg .L8
.L7:
movl $-1, %eax
jmp .L9
.L8:
movq -56(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %edx
movq -24(%rbp), %rax
movl %edx, (%rax)
movq -8(%rbp), %rax
addq $4, %rax
movl (%rax), %edx
movq -32(%rbp), %rax
movl %edx, (%rax)
movq -8(%rbp), %rax
addq $8, %rax
movl (%rax), %edx
movq -40(%rbp), %rax
movl %edx, (%rax)
movq -8(%rbp), %rax
addq $12, %rax
movl (%rax), %edx
movq -48(%rbp), %rax
movl %edx, (%rax)
movl $0, %eax
parse_package2:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq %rdx, -40(%rbp)
movq %rcx, -48(%rbp)
movq %r8, -56(%rbp)
movl %r9d, -60(%rbp)
cmpq $0, -56(%rbp)
je .L7
cmpl $15, -60(%rbp)
jg .L8
.L7:
movl $-1, %eax
jmp .L9
.L8:
movq -56(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %edx
movq -24(%rbp), %rax
movl %edx, (%rax)
movq -8(%rbp), %rax
addq $4, %rax
movl (%rax), %edx
movq -32(%rbp), %rax
movl %edx, (%rax)
movq -8(%rbp), %rax
addq $8, %rax
movl (%rax), %edx
movq -40(%rbp), %rax
movl %edx, (%rax)
movq -8(%rbp), %rax
addq $12, %rax
movl (%rax), %edx
movq -48(%rbp), %rax
movl %edx, (%rax)
movl $0, %eax
L8是主段落,對a的賦值:
26-29行,一共4行解決。
這個例子中強制轉換(parse_package2) 比內存復制(parse_package)要少2倍的CPU指令,性能至少可以提高2倍。
因此,我們的開發中應該盡量減少對內存復制的使用,而應該采用強制轉換,現在64位服務器上,我們甚至可以用8個字節的long,下面的例子:
[cpp]
long lv;
char buffer[ 8 ];
memcpy( &lv, buffer, 8 );
lv = *(int*)(buffer);
long lv;
char buffer[ 8 ];
memcpy( &lv, buffer, 8 );
lv = *(int*)(buffer);
這樣就能更好的利用CPU的多字節指令提高性能。