내 눈 에 보 이 는 Nginx (1): Nginx 와 비트 연산

12623 단어
작가 장 초: 클 라 우 드 시스템 개발 고급 엔지니어 도 찍 고 클 라 우 드 CDN 플랫폼 관련 구성 요소 의 업데이트 와 유 지 를 책임 집 니 다.Github ID: tokers 는 OpenResty 커 뮤 니 티 와 Nginx 메 일 링 리스트 등 오픈 소스 커 뮤 니 티 에서 활동 하 며 서버 기술 에 대한 연구 에 전념 합 니 다.일찍이 ngxlua 공헌 원본, Nginx, ngxlua, CDN 성능 최적화, 로그 최적화 에 대해 심도 있 는 연구 가 있 습 니 다.
Nginx 는 성능 으로 유명 하 다. 이것 은 우수한 코드 실현 과 밀접 한 관 계 를 가진다. 본 고 에서 말 하고 자 하 는 비트 연산 도 Nginx 의 우수한 성능 을 추진 하 는 원인 중 하나 이다.
비트 연산 은 Nginx 의 소스 코드 에서 곳곳에서 볼 수 있 습 니 다. 명령 의 유형 (얼마나 많은 파 라 메 터 를 휴대 할 수 있 고 어떤 설정 블록 에 나타 날 수 있 습 니까?) 을 정의 하 는 것 부터 현재 요청 이 완료 되 지 않 은 데이터 가 있 는 지 표시 하 는 것, 그리고 Nginx 이벤트 모듈 에서 포인터 의 최 하위 로 이벤트 가 만 료 되 었 는 지 표시 하 는 것 까지 비트 연산 의 신기 함 과 매력 을 나타 내지 않 습 니 다.
본 고 는 Nginx 소스 코드 의 전형 적 인 비트 연산 사용 을 소개 하고 분석 하 며 다른 비트 연산 기 교 를 확장 하여 소개 할 것 이다.
맞추다
Nginx 내부 에서 메모리 분 배 를 할 때 메모리 시작 주소 의 정렬, 즉 메모리 정렬 (일부 성능 향상 으로 바 꿀 수 있 음) 에 매우 주의 합 니 다. 이것 은 프로세서 의 주소 지정 특성 과 관련 이 있 습 니 다. 예 를 들 어 일부 프로세서 는 4 바이트 너비 로 주 소 를 찾 습 니 다. 이런 기계 에 서 는 0x46b1e 7 에서 시 작 된 4 개의 바이트 를 읽 어야 합 니 다.0x46b1e 7 은 4 바이트 경계 (0x46b1e 7% 4 = 3) 에 있 지 않 기 때문에 읽 을 때 두 번 으로 나 누 어 읽 습 니 다. 0x46b1e 4 에서 시 작 된 4 개의 바이트 를 처음 읽 고 3 바이트 낮 게 꺼 냅 니 다.0x46b1e 8 에서 시 작 된 4 개의 바 이 트 를 읽 고 가장 높 은 바 이 트 를 꺼 냅 니 다.읽 기와 쓰기 메모리 의 속도 가 CPU 와 일치 하지 않 는 다 는 것 을 알 고 있 습 니 다. 그러면 두 번 의 읽 기 는 분명히 더 큰 비용 을 가 져 왔 습 니 다. 이것 은 명령 의 정 체 를 일 으 키 고 CPI (명령 주기 수) 를 증가 시 켜 프로그램의 성능 을 손상 시 킬 수 있 습 니 다.
그래서 Nginx 는 정렬 작업 을 하기 위해 매크로 를 봉 인 했 습 니 다.
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))


위의 코드 에서 보 듯 이 이 매크로 는 d 를 a 로 정렬 시 키 는데 그 중에서 a 는 반드시 2 의 미터 가 되 어야 합 니 다.
예 를 들 어 d 는 17 이 고 a 는 2 일 때 18 을 얻는다.d 는 15, a 는 4 시 에 16 을 얻는다.d 는 16, a 는 4 시, 16 을 얻는다.
이 매크로 는 사실 d 와 같은 첫 번 째 a 의 배 수 를 찾 고 있다.a 는 2 의 幂 次 이기 때문에 a 의 2 진법 은 00... 1... 00 이라는 형식 을 나타 낸다. 즉, 그것 은 1 개 만 있 기 때문에 a - 1 은 00... 01. 1 이라는 형식 이다. 그러면 ~ (a - 1) 은 낮은 n 위 를 모두 0 으로 설정 하고 그 중에서 n 은 a 낮은 연속 0 의 개수 이다.그래서 이때 우리 가 d 와 ~ (a - 1) 에 게 위치 와 조작 을 한 번 하 라 고 하면 d 의 낮은 n 자 리 를 0 으로 정리 할 수 있 습 니 다. 우 리 는 d 와 같은 수 를 찾 아야 하기 때문에 d + (a - 1) 를 사용 하면 됩 니 다.
비트 맵
비트 맵 은 보통 사물 의 상 태 를 표시 하 는데 '비트' 는 모든 사물 이 하나의 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트 비트
Nginx 에는 공유 메모리 분배 기 (slab) 와 같은 비트 맵 을 사용 하 는 예 가 여러 군데 있 습 니 다. 예 를 들 어 uri (Uniform Resource Identifier) 를 바 꿀 때 하나의 문자 가 보존 문자 인지 (또는 안전 하지 않 은 문자 인지) 판단 해 야 합 니 다. 이러한 문 자 는% XX 로 바 뀌 어야 합 니 다.
static uint32_t   uri_component[] = {
        0xffffffff, /* 1111 1111 1111 1111  1111 1111 1111 1111 */

/* ?>=< ;:98 7654 3210  /.-, +*)( '&%$ #"!  */
        0xfc009fff, /* 1111 1100 0000 0000  1001 1111 1111 1111 */

/* _^]\ [ZYX WVUT SRQP  ONML KJIH GFED CBA@ */
        0x78000001, /* 0111 1000 0000 0000  0000 0000 0000 0001 */

/*  ~}| {zyx wvut srqp  onml kjih gfed cba` */
        0xb8000001, /* 1011 1000 0000 0000  0000 0000 0000 0001 */

        0xffffffff, /* 1111 1111 1111 1111  1111 1111 1111 1111 */
        0xffffffff, /* 1111 1111 1111 1111  1111 1111 1111 1111 */
        0xffffffff, /* 1111 1111 1111 1111  1111 1111 1111 1111 */
        0xffffffff  /* 1111 1111 1111 1111  1111 1111 1111 1111 */
    };


위 에서 보 듯 이 간단 한 배열 은 하나의 비트 맵 을 구성 하고 모두 8 개의 숫자 를 포함 하 며 모든 숫자 는 32 개의 상 태 를 표시 하기 때문에 이 비트 맵 은 256 개의 문자 (확장 ASCII 코드 포함) 를 포함한다.0 의 비트 는 일반적인 문 자 를 표시 합 니 다. 즉, 전의 가 필요 없고 1 의 비트 를 대표 하 는 것 은 전의 가 필요 합 니 다.
그럼 이 비트 맵 은 어떻게 사용 해 야 합 니까?Nginx 는 uri 를 옮 겨 다 닐 때 간단 한 문 구 를 통 해 판단 한다.
uri_component[ch >> 5] & (1U << (ch & 0x1f))


위 에서 보 듯 이 ch 는 현재 문 자 를 표시 합 니 다. ch > > 5 는 ch 오른쪽으로 5 자리 이동 합 니 다. 이것 은 32 로 나 누 는 효 과 를 가 집 니 다. 이 작업 은 ch 가 uri 에 있 음 을 확 인 했 습 니 다.component 의 몇 번 째 숫자 에서;오른쪽 에 있 는, (ch & 0x1f) 는 ch 가 5 자리 낮은 값 을 꺼 냈 는데, 이 값 은 ch 가 대응 하 는 숫자의 몇 번 째 자리 (낮은 값 에서 높 은 계산) 에 있 음 을 나타 낸다.따라서 좌우 양쪽 의 값 을 한 번 에 위치 와 조작 을 한 후에 ch 문자 가 있 는 비트 맵 상 태 를 꺼 냈 다.예 를 들 어 ch 는 '0' (즉 숫자 48) 으로 비트 맵 의 두 번 째 숫자 (48 > > 5 = 1) 에 존재 하고 이 숫자 (0xfc 009 fff) 의 16 위 에 존재 하기 때문에 그의 상 태 는 0xfc 009 fff & 0x 10000 = 0 이기 때문에 '0' 은 통용 되 는 문자 로 의 미 를 바 꾸 지 않 아 도 된다.
위의 이 예 에서 우 리 는 또 다른 비트 연산 의 기 교 를 볼 수 있다. 즉, 2 의 멱 차 의 수 를 모델 링 하거나 제거 작업 을 할 때 비트 연산 을 통 해 실현 할 수 있다. 이것 은 직접적인 나눗셈 과 모델 링 연산 보다 더 좋 은 성능 을 가진다. 비록 적당 한 최적화 단계 에서 컴 파일 러 도 우 리 를 위해 이런 최적화 를 완성 할 수 있다.
최 하위 1 의 위 치 를 찾다
이어서 우 리 는 다른 응용 기 교 를 소개 한다.
숫자 2 진법 에서 가장 낮은 1 의 위 치 를 찾 으 면 직관 적 으로 위치 에 따라 옮 겨 다 니 는 것 을 생각 할 수 있 습 니 다. 이런 알고리즘 은 시간 이 복잡 하고 O (n) 이 며 성능 이 만 족 스 럽 지 못 합 니 다.
만약 당신 이 나무 모양 의 배열 을 접 한 적 이 있다 면, 당신 은 이것 에 대해 다른 견 해 를 가지 게 될 것 입 니 다. 나무 모양 배열 의 핵심 개념 은 lowbit 를 계산 하 는 것 입 니 다. 즉, 하나의 숫자 2 진법 에서 가장 낮은 1 의 멱 차 를 계산 하 는 것 입 니 다.그것 이 좋 은 시간 복잡 도 (O (logN) 를 가 진 이 유 는 O (1) 나 상수 의 시간 안에 답 을 얻 을 수 있 기 때문이다.
int lowbit(int x)
{
    return x & ~(x - 1);
}


이 기 교 는 사실상 상술 한 정렬 방식 과 유사 하 다. 예 를 들 어 x 는 00... 111000 이라는 숫자 는 x - 1 이 00... 110111 이 되 고 반대로 하면 원래 x 저 위 연속 0 이 있 는 위 치 를 0 으로 다시 놓는다 (원래 최 저 위 1 의 위 치 는 1 이다). 우 리 는 최 저 위 1 의 위 치 를 제외 하고다른 위치 에 있 는 값 과 x 는 모두 반대 되 기 때문에 이들 이 위치 와 조작 을 한 후에 결과 에 1 만 있 을 수 있 고 원래 x 의 가장 낮은 1 이다.
가장 높 은 자리 1 찾기
문 제 를 바 꾸 자 면 이번 에는 최 하위 가 아니 라 최고 위 를 찾 는 1 이다.
이 문 제 는 실제 적 인 의 미 를 가진다. 예 를 들 어 best - fit 메모리 풀 을 설계 할 때 우 리 는 사용자 가 기대 하 는 size 보다 큰 첫 번 째 2 의 멱 을 찾 아야 한다.
마찬가지 로, 너 는 아마도 먼저 두루 돌아 다 니 는 것 을 생각 할 것 이다.
사실 Intel CPU 명령 집합 에는 이러한 명령 이 있 습 니 다. 바로 하나의 바 이 너 리 에서 가장 높 은 1 의 위 치 를 계산 하 는 것 입 니 다.
size_t bsf(size_t input)
{
    size_t pos;

    __asm__("bsfq %1, %0" : "=r" (pos) : "rm" (input));

    return pos;
}


이것 은 매우 좋 지만, 여기 서 우 리 는 여전히 비트 연산 으로 이 1 의 위 치 를 찾 기 를 기대한다.
size_t bsf(size_t input)
{
    input |= input >> 1;
    input |= input >> 2;
    input |= input >> 4;
    input |= input >> 8;
    input |= input >> 16;
    input |= input >> 32;

    return input - (input >> 1);
}


이것 이 바로 우리 가 기대 하 는 계산 방식 이다.우 리 는 이 계산의 원 리 를 분석 해 보 자.
설명 이 필요 한 것 은 계산 해 야 할 값 이 32 자리 라면 위의 함수 의 마지막 input | = input > > 32 는 필요 하지 않 습 니 다. input | = input > > m 를 구체 적 으로 몇 번 실행 하 는 지 는 input 의 비트 길이 에 의 해 결 정 됩 니 다. 예 를 들 어 8 자 리 는 3 번, 16 자 리 는 4 번, 32 자 리 는 5 번 입 니 다.
더욱 간결 하 게 설명 하기 위해 서 우 리 는 8 자리 숫자 로 분석 하고 하나의 숫자 A 를 설정 합 니 다. 그의 바 이 너 리 는 다음 과 같 습 니 다.
A[7] A[6] A[5] A[4] A[3] A[2] A[1] A[0]


위의 계산 과정 은 다음 과 같다.
A[7] A[6] A[5] A[4] A[3] A[2] A[1] A[0]
0    A[7] A[6] A[5] A[4] A[3] A[2] A[1]
---------------------------------------
A[7] A[7]|A[6] A[6]|A[5] A[5]|A[4] A[4]|A[3] A[3]|A[2] A[2]|A[1] A[1]|A[0]
0    0         A[7]      A[7]|A[6] A[6]|A[5] A[5]|A[4] A[4]|A[3] A[3]|A[2]
--------------------------------------------------------------------------
A[7] A[7]|A[6] A[7]|A[6]|A[5] A[7]|A[6]|A[5]|A[4] A[6]|A[5]|A[4]|A[3] A[5]|A[4]|A[3]|A[2] A[4]|A[3]|A[2]|A[1] A[3]|A[2]|A[1]|A[0]
0    0         0              0                   A[7]                A[7]|A[6]           A[7]|A[6]|A[5]      A[7]|A[6]|A[5]|A[4]
---------------------------------------------------------------------------------------------------------------------------------
A[7] A[7]|A[6] A[7]|A[6]|A[5]  A[7]|A[6]|A[5]|A[4] A[7]|A[6]|A[5]|A[4]|A[3] A[7]|A[6]|A[5]|A[4]|A[3]|A[2] A[7]|A[6]|A[5]|A[4]|A[3]|A[2]|A[1] A[7]|A[6]|A[5]|A[4]|A[3]|A[2]|A[1]|A[0]


우 리 는 최종 A 의 최고 위 치 는 A [7] 이 고, 다음 위 치 는 A [7] | A [6] 이 며, 세 번 째 위 치 는 A [7] | A [6] | A [5] | A [2] | A [1] | A [7] | A [6] | A [5] | A [4] | A [3] | A [2] | A [1] | A [0] | A [0] 를 볼 수 있다.
가장 높 은 1 이 m 위 (오른쪽 에서 왼쪽으로 계산 하고 가장 낮은 것 을 0 위 라 고 한다) 라 고 가정 하면 이때 의 낮은 m 위 는 모두 1 이 고 다른 높 은 위 는 모두 0 이다.즉, A 는 2 의 모 멱 을 하나 더 줄 일 것 이 므 로 마지막 단계 (input - (input > > 1) 의 의도 도 매우 뚜렷 하 다. 곧 최고 위 를 제외 한 1 을 모두 0 으로 설정 하고 마지막 으로 돌아 오 는 것 은 원래 의 input 에서 가장 높 은 1 의 대응 멱 이다.
계산 1 의 개수
어떻게 하나의 숫자 이 진 표시 안에 몇 개의 1 이 있 는 지 계산 합 니까?
직관 적 으로 (옮 겨 다 니 는 것 은 정말 좋 은 것 이다) 복잡 도 를 계산 해 보 자. 한 바이트 가 O (8) 이 고 네 바이트 가 O (32) 이 며 8 바이트 가 O (64) 이다.
만약 이 계산 이 당신 의 프로그램 에 빈번하게 나타난다 면, 당신 이 perf 와 같은 성능 분석 도구 로 당신 의 응용 프로그램 을 관찰 할 때, 그것 은 아마도 당신 의 관심 을 받 을 것 이 고, 당신 은 어 쩔 수 없 이 최적화 할 방법 을 생각해 야 한다.
사실은 이라는 책 에 이 문제 가 있 습 니 다. 이 문 제 는 기호 가 없 는 긴 숫자 2 진법 에서 1 의 수 를 계산 해 야 합 니 다. 그리고 가장 좋 은 알고리즘 을 사용 하 기 를 바 랍 니 다. 최종 적 으로 이 알고리즘 의 복잡 도 는 O (8) 입 니 다.
long fun_c(unsigned long x)
{
    long val = 0;
    int i;
    for (i = 0; i < 8; i++) {
        val += x & 0x0101010101010101L;
        x >>= 1;
    }

    val += val >> 32;
    val += val >> 16;
    val += val >> 8;

    return val & 0xFF;
}


이 알고리즘 은 나의 다른 문장 에서 분석 한 적 이 있다.
0x 0101010101010101 이라는 수 를 관찰 하면 8 명 중 마지막 한 명 만 1 이다.그러면 x 와 그 위 치 를 따 르 면 다음 과 같은 결 과 를 얻 을 수 있 습 니 다.
  A[i]    x         i    (0   1)。
   :
A[0] + (A[8] << 8) + (A[16] << 16) + (A[24] << 24) + (A[32] << 32) + (A[40] << 40) + (A[48] << 48) + (A[56] << 56)
   :
A[1] + (A[9] << 8) + (A[17] << 16) + (A[25] << 24) + (A[33] << 32) + (A[41] << 40) + (A[49] << 48) + (A[57] << 56)
......
   :
A[7] + (A[15] << 8) + (A[23] << 16) + (A[31] << 24) + (A[39] << 32) + (A[47] << 40) + (A[55] << 48) + (A[63] << 56)
        :
(A[63] + A[62] + A[61] + A[60] + A[59] + A[58] + A[57] + A[56]) << 56 +
(A[55] + A[54] + A[53] + A[52] + A[51] + A[50] + A[49] + A[48]) << 48 +
(A[47] + A[46] + A[45] + A[44] + A[43] + A[42] + A[41] + A[40]) << 40 +
(A[39] + A[38] + A[37] + A[36] + A[35] + A[34] + A[33] + A[32]) << 32 +
(A[31] + A[30] + A[29] + A[28] + A[27] + A[26] + A[25] + A[24]) << 24 +
(A[23] + A[22] + A[21] + A[20] + A[19] + A[18] + A[17] + A[16]) << 16 +
(A[15] + A[14] + A[13] + A[12] + A[11] + A[10] + A[9]  + A[8])  << 8  +
(A[7]  + A[6]  + A[5]  + A[4]  + A[3]  + A[2]  + A[1]  + A[0])


다음 세 가지 조작:
val += val >> 32;
val += val >> 16;
val += val >> 8;


매번 val 을 반 으로 접어 서 더 합 니 다.
첫 번 째 반 으로 접 기 (val + = val > > 32) 후 얻 은 val 의 낮은 32 비트:
(A[31] + A[30] + A[29] + A[28] + A[27] + A[26] + A[25] + A[24] + A[63] + A[62] + A[61] + A[60] + A[59] + A[58] + A[57] + A[56]) << 24 +
(A[23] + A[22] + A[21] + A[20] + A[19] + A[18] + A[17] + A[16] + A[55] + A[54] + A[53] + A[52] + A[51] + A[50] + A[49] + A[48]) << 16 +
(A[15] + A[14] + A[13] + A[12] + A[11] + A[10] + A[9]  + A[8] + A[47] + A[46] + A[45] + A[44] + A[43] + A[42] + A[41] + A[40])  << 8  +
(A[7]  + A[6]  + A[5]  + A[4]  + A[3]  + A[2]  + A[1]  + A[0] + A[39] + A[38] + A[37] + A[36] + A[35] + A[34] + A[33] + A[32])


두 번 째 반 으로 접 기 (val + = val > > 16) 후 얻 은 val 의 낮은 16 비트:
15] + A[14] + A[13] + A[12] + A[11] + A[10] + A[9]  + A[8] + A[47] + A[46] + A[45] + A[44] + A[43] + A[42] + A[41] + A[40] + A[31] + A[30] + A[29] + A[28] + A[27] + A[26] + A[25] + A[24] + A[63] + A[62] + A[61] + A[60] + A[59] + A[58] + A[57] + A[56])  << 8  +
(A[7]  + A[6]  + A[5]  + A[4]  + A[3]  + A[2]  + A[1]  + A[0] + A[39] + A[38] + A[37] + A[36] + A[35] + A[34] + A[33] + A[32] + A[23] + A[22] + A[21] + A[20] + A[19] + A[18] + A[17] + A[16] + A[55] + A[54] + A[53] + A[52] + A[51] + A[50] + A[49] + A[48])


세 번 째 반 으로 접 기 (val + = val > > 8) 후 얻 은 val 의 낮은 8 자리:
(A[7]  + A[6]  + A[5]  + A[4]  + A[3]  + A[2]  + A[1]  + A[0] + A[39] + A[38] + A[37] + A[36] + A[35] + A[34] + A[33] + A[32] + A[23] + A[22] + A[21] + A[20] + A[19] + A[18] + A[17] + A[16] + A[55] + A[54] + A[53] + A[52] + A[51] + A[50] + A[49] + A[48] + A[15] + A[14] + A[13] + A[12] + A[11] + A[10] + A[9]  + A[8] + A[47] + A[46] + A[45] + A[44] + A[43] + A[42] + A[41] + A[40] + A[31] + A[30] + A[29] + A[28] + A[27] + A[26] + A[25] + A[24] + A[63] + A[62] + A[61] + A[60] + A[59] + A[58] + A[57] + A[56])


이 를 통 해 알 수 있 듯 이 세 번 의 반절 을 거 쳐 64 자리 의 수 치 를 모두 8 자리 까지 누적 한 다음 에 8 자리 낮은 수 치 를 꺼 내 는 것 이 바로 x 라 는 숫자 2 진법 에서 1 의 숫자 이다. 이 문 제 는 수학 적 으로 '한 명 무 게 를 계산 하 는 것' 이 라 고 부른다.
비트 연산 은 독특한 장점 (간결, 성능 봉) 으로 프로그래머 를 끌 어 들 인 다. 예 를 들 어 LuaJIT 는 bit 라 는 모듈 을 내장 하여 프로그래머 가 Lua 프로그램 에서 비트 연산 을 사용 할 수 있 도록 한다.비트 연산 을 배 우 는 것 은 프로그래머 에 게 도 진보 이 므 로 우리 가 계속 연구 할 만하 다.
추천 읽 기:
클 라 우 드 오픈 레 스 티 / Nginx 서비스 최적화 실천
클 라 우 드 딩 설 봉: 자체 캐 시 구성 요소 BearCache, CDN 디스크 응답 속도 38%

좋은 웹페이지 즐겨찾기