Program Tip

캐시 라인 크기에 언제 어떻게 정렬해야합니까?

programtip 2020. 12. 27. 19:55
반응형

캐시 라인 크기에 언제 어떻게 정렬해야합니까?


C ++로 작성된 Dmitry Vyukov의 뛰어난 경계 mpmc 대기열에서 참조 : http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue

그는 패딩 변수를 추가합니다. 나는 이것이 성능을 위해 캐시 라인에 맞추는 것이라고 생각합니다.

질문이 몇 개 있습니다.

  1. 왜 이런 식으로 수행됩니까?
  2. 항상 작동하는 휴대용 방법입니까?
  3. 어떤 경우에 __attribute__ ((aligned (64)))대신 사용하는 것이 가장 좋습니다.
  4. 버퍼 포인터 앞에 패딩이 성능에 도움이되는 이유는 무엇입니까? 포인터가 캐시에로드 된 것이 아니므로 실제로 포인터의 크기 일 뿐입니 까?

    static size_t const     cacheline_size = 64;
    typedef char            cacheline_pad_t [cacheline_size];
    
    cacheline_pad_t         pad0_;
    cell_t* const           buffer_;
    size_t const            buffer_mask_;
    cacheline_pad_t         pad1_;
    std::atomic<size_t>     enqueue_pos_;
    cacheline_pad_t         pad2_;
    std::atomic<size_t>     dequeue_pos_;
    cacheline_pad_t         pad3_;
    

이 개념은 c 코드의 gcc에서 작동합니까?


이렇게하면 다른 필드를 수정하는 다른 코어가 캐시 사이에 두 필드를 모두 포함하는 캐시 라인을 바운스 할 필요가 없습니다. 일반적으로 프로세서가 메모리의 일부 데이터에 액세스하려면이를 포함하는 전체 캐시 라인이 해당 프로세서의 로컬 캐시에 있어야합니다. 해당 데이터를 수정하는 경우 해당 캐시 항목은 일반적으로 시스템의 모든 캐시에있는 유일한 복사본이어야합니다 (MESI / MOESI 스타일 캐시 일관성 프로토콜의 독점 모드 ). 별도의 코어가 동일한 캐시 라인에있는 다른 데이터를 수정하려고 시도하여 전체 라인을 앞뒤로 이동하는 데 시간을 낭비하는 것을 허위 공유 라고 합니다 .

제공하는 특정 예에서 한 코어는 항목을 대기열에 추가 (읽기 (공유) buffer_및 쓰기 (배타적) 전용 enqueue_pos_)하는 반면 다른 코어는 다른 코어가 소유 한 캐시 라인에서 중단 되지 않고 대기열에서 제외 (공유 buffer_및 배타 dequeue_pos_) 할 수 있습니다.

시작 수단에서의 패딩 buffer_buffer_mask_오히려 두 줄에 걸쳐 분할보다, 같은 캐시 라인에 결국 때문에 접근 더블 메모리 트래픽을 필요로.

이 기술이 완전히 이식 가능한지 확실하지 않습니다. 각각 cacheline_pad_t은 64 바이트 (해당 크기) 캐시 라인 경계에 맞춰 정렬되므로 그 뒤에 오는 모든 것은 다음 캐시 라인에 배치됩니다. 내가 아는 한, C 및 C ++ 언어 표준은 전체 구조 만 필요하므로 멤버의 정렬 요구 사항을 위반하지 않고 배열에 멋지게 살 수 있습니다. (댓글 참조)

attribute접근 방식은 더 컴파일러 구체적으로,하지만 패딩은 전체 캐시 라인에 각 요소를 반올림 제한 될 것이기 때문에, 절반이 구조의 크기를 잘라 수 있습니다. 이것들이 많이 있으면 꽤 유익 할 수 있습니다.

동일한 개념이 C와 C ++에 적용됩니다.


인터럽트 또는 고성능 데이터 읽기로 작업 할 때 일반적으로 캐시 라인 당 64 바이트 인 캐시 라인 경계에 맞춰야 할 수 있으며 프로세스 간 소켓으로 작업 할 때 반드시 사용해야합니다. 프로세스 간 소켓을 사용하면 여러 캐시 라인 또는 DDR RAM 워드에 분산 될 수없는 제어 변수가 있습니다. 그렇지 않으면 L1, L2 등 또는 캐시 또는 DDR RAM이 저역 통과 필터로 작동하고 인터럽트 데이터를 필터링합니다. ! 그건 나빠!!! 즉, 알고리즘이 좋을 때 기이 한 오류가 발생하고 미치게 만들 가능성이 있습니다!

DDR RAM은 거의 항상 16 바이트 인 128 비트 워드 (DDR RAM 워드)로 읽을 것이므로 링 버퍼 변수는 여러 DDR RAM 워드에 분산되어 있지 않습니다. 일부 시스템은 64 비트 DDR RAM 워드를 사용하며 기술적으로는 16 비트 CPU에서 32 비트 DDR RAM 워드를 얻을 수 있지만 상황에서는 SDRAM을 사용합니다.

고성능 알고리즘으로 데이터를 읽을 때 사용되는 캐시 라인 수를 최소화하는 데 관심이있을 수도 있습니다. 제 경우에는 세계에서 가장 빠른 정수-문자열 알고리즘 (이전의 가장 빠른 알고리즘보다 40 % 빠름)을 개발했으며 세계에서 가장 빠른 부동 소수점 알고리즘 인 Grisu 알고리즘을 최적화하기 위해 노력하고 있습니다. 부동 소수점 숫자를 인쇄하려면 정수를 인쇄해야하므로 Grisu 최적화를 최적화하려면 Grisu에 대한 LUT (Lookup Tables)를 정확히 15 개의 캐시 라인으로 캐시 라인 정렬했습니다. 실제로 그렇게 정렬 된 것이 다소 이상합니다. 이는 .bss 섹션 (예 : 정적 메모리)에서 LUT를 가져와 스택 (또는 힙이지만 스택이 더 적합 함)에 배치합니다. 나는 이것을 벤치마킹하지 않았지만 기르는 것이 좋습니다. 값을로드하는 가장 빠른 방법은 d- 캐시가 아닌 i- 캐시에서로드하는 것입니다. 차이점은 i-cache는 읽기 전용이고 읽기 전용이기 때문에 캐시 라인이 훨씬 더 크다는 것입니다 (2KB는 교수님이 한 번 인용 한 것입니다.). 따라서 실제로 다음과 같은 변수를로드하는 것과 반대로 배열 인덱싱에서 성능을 저하시킬 것입니다.

int faster_way = 12345678;

느린 방법과는 반대로 :

int variables[2] = { 12345678, 123456789};
int slower_way = variables[0];

차이점은 int variable = 12345678함수 시작부터 i-cache의 변수로 오프셋하여 i-cache 라인에서 slower_way = int[0]로드되는 반면 훨씬 느린 배열 인덱싱을 사용하여 더 작은 d- 캐시 라인에서로드된다는 것입니다. 제가 방금 발견 한이 미묘하게도 실제로 저와 다른 많은 정수-문자열 알고리즘을 느리게 만듭니다. 그렇지 않은 경우 읽기 전용 데이터를 캐시로 정렬하여 최적화하는 것이기 때문입니다.

일반적으로 C ++에서는 std::align함수 를 사용합니다 . 이 기능 은 최적으로 작동하지 않을 수 있으므로 사용하지 않는 것이 좋습니다. 다음은 캐시 라인에 정렬하는 가장 빠른 방법입니다. 내가 바로 저자이고 이것은 뻔뻔한 플러그입니다.

Kabuki 툴킷 메모리 정렬 알고리즘

namespace _ {
/* Aligns the given pointer to a power of two boundaries with a premade mask.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number of bits in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param mask The mask for the Least Significant bits to align. */
template <typename T = char>
inline T* AlignUp(void* pointer, intptr_t mask) {
  intptr_t value = reinterpret_cast<intptr_t>(pointer);
  value += (-value ) & mask;
  return reinterpret_cast<T*>(value);
}
} //< namespace _

// Example calls using the faster mask technique.

enum { kSize = 256 };
char buffer[kSize + 64];

char* aligned_to_64_byte_cache_line = AlignUp<> (buffer, 63);

char16_t* aligned_to_64_byte_cache_line2 = AlignUp<char16_t> (buffer, 63);

and here is the faster std::align replacement:

inline void* align_kabuki(size_t align, size_t size, void*& ptr,
                          size_t& space) noexcept {
  // Begin Kabuki Toolkit Implementation
  intptr_t int_ptr = reinterpret_cast<intptr_t>(ptr),
           offset = (-int_ptr) & (align - 1);
  if ((space -= offset) < size) {
    space += offset;
    return nullptr;
  }
  return reinterpret_cast<void*>(int_ptr + offset);
  // End Kabuki Toolkit Implementation
}

ReferenceURL : https://stackoverflow.com/questions/8469427/how-and-when-to-align-to-cache-line-size

반응형