【问题标题】:Improving performance of generic swap提高通用交换的性能
【发布时间】:2020-12-30 12:39:05
【问题描述】:

上下文

在 C 中实现适用于多种类型的泛型函数时,经常使用void*libc 函数 qsort() 就是一个经典的例子。内部qsort() 和许多其他算法需要swap() 函数。

一个简单但典型的通用交换实现如下所示:

void swap(void* x, void* y, size_t size) {
    char t[size];
    memcpy(t, x, size);
    memcpy(x, y, size);
    memcpy(y, t, size);
}

对于较大的类型,可以使用逐字节交换,或者使用malloc,这会很慢,但这里的重点是当这个泛型swap() 用于小型类型时会发生什么。

更好的通用交换?

事实证明,如果我们匹配一些常见的类型大小(x86_64 上的 4 和 8 字节的 int 和 long)也包括 float、double、pointer 等,我们可以获得令人惊讶的性能提升:

void swap(void* x, void* y, size_t size) {
  if (size == sizeof(int)) {
    int t      = *((int*)x);
    *((int*)x) = *((int*)y);
    *((int*)y) = t;
  } else if (size == sizeof(long)) {
    long t      = *((long*)x);
    *((long*)x) = *((long*)y);
    *((long*)y) = t;
  } else {
    char t[size];
    memcpy(t, x, size);
    memcpy(x, y, size);
    memcpy(y, t, size);
  }
}

注意:这显然可以改进为使用 #if 而不是 if/else 以及更多类型。

在以下通用 quicksort() 实现的上下文中,与更标准的 memcpy() 仅顶部交换相比,上述交换为 10,000,000 随机 int 排序提供了约 2 倍的性能提升。这是在 ubuntu 20.04 上使用 gcc-9 或 clang-10 和 -O3

问题

这似乎是一个了不起的结果。

  • 这是否违反任何标准?
  • 谁能验证这一点?
  • 是什么让这种收益成为可能?它是简单地复制“更广泛的词”还是一些编译器优化/内联在起作用?
  • 如果确实有效,为什么还没有完成?是吗?

注意:我还没有检查生成的汇编代码。

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

typedef bool (*cmp)(const void*, const void*);

bool cmp_ints_less(const void* a, const void* b) {
  return *(const int*)a < *(const int*)b;
}

bool cmp_ints_greater(const void* a, const void* b) {
  return *(const int*)a > *(const int*)b;
}

bool cmp_floats_less(const void* a, const void* b) {
  return *(const float*)a < *(const float*)b;
}

bool cmp_floats_greater(const void* a, const void* b) {
  return *(const float*)a > *(const float*)b;
}

bool cmp_doubles_less(const void* a, const void* b) {
  return *(const double*)a < *(const double*)b;
}

bool cmp_doubles_greater(const void* a, const void* b) {
  return *(const double*)a > *(const double*)b;
}

bool cmp_strs_less(const void* a, const void* b) {
  return strcmp(*((const char**)a), *((const char**)b)) < 0;
}

bool cmp_strs_greater(const void* a, const void* b) {
  return strcmp(*((const char**)a), *((const char**)b)) > 0;
}

void swap(void* x, void* y, size_t size) {
  if (size == sizeof(int)) {
    int t      = *((int*)x);
    *((int*)x) = *((int*)y);
    *((int*)y) = t;
  } else if (size == sizeof(long)) {
    long t      = *((long*)x);
    *((long*)x) = *((long*)y);
    *((long*)y) = t;
  } else {
    char t[size];
    memcpy(t, x, size);
    memcpy(x, y, size);
    memcpy(y, t, size);
  }
}

void* partition(void* start, void* end, size_t size, cmp predicate) {
  if (start == NULL || end == NULL || start == end) return start;
  char* storage = (char*)start;
  char* last    = (char*)end - size; // used as pivot
  for (char* current = start; current != last; current += size) {
    if (predicate(current, last)) {
      swap(current, storage, size);
      storage += size;
    }
  }
  swap(storage, last, size);
  return storage; // returns position of pivot
}

void quicksort(void* start, void* end, size_t size, cmp predicate) {
  if (start == end) return;
  void* middle = partition(start, end, size, predicate);
  quicksort(start, middle, size, predicate);
  quicksort((char*)middle + size, end, size, predicate);
}

void print(const int* start, int size) {
  for (int i = 0; i < size; ++i) printf("%3d", start[i]);
  printf("\n");
}

void rand_seed() {
  int   seed = 0;
  FILE* fp   = fopen("/dev/urandom", "re");
  if (!fp) {
    fprintf(stderr, "Warning: couldn't open source of randomness, falling back to time(NULL)");
    srand(time(NULL));
    return;
  }
  if (fread(&seed, sizeof(int), 1, fp) < 1) {
    fprintf(stderr, "Warning: couldn't read random seed, falling back to time(NULL)");
    fclose(fp);
    srand(time(NULL));
    return;
  }
  fclose(fp);
  srand(seed); // nice seed for rand()
}

int rand_range(int start, int end) {
  return start + rand() / (RAND_MAX / (end - start + 1) + 1);
}

int main() {
  // int demo
  rand_seed();
#define int_count 20
  int* ints = malloc(int_count * sizeof(int));
  if (!ints) {
    fprintf(stderr, "couldn't allocate memory");
    exit(EXIT_FAILURE);
  }
  for (int i = 0; i < int_count; ++i) ints[i] = rand_range(1, int_count / 2);
  print(ints, int_count);
  quicksort(ints, ints + int_count, sizeof(int), &cmp_ints_less);
  print(ints, int_count);
  free(ints);

  // string demo
  const char* strings[] = {
      "material", "rare",    "fade",      "aloof",  "way",  "torpid",
      "men",      "purring", "abhorrent", "unpack", "zinc", "unsightly",
  };
  const int str_count = sizeof(strings) / sizeof(strings[0]);
  quicksort(strings, strings + str_count, sizeof(char*), &cmp_strs_greater);
  for (int i = 0; i < str_count; ++i) printf("%s\n", strings[i]);

// double demo
#define dbl_count 20
  double doubles[dbl_count];
  for (int i = 0; i < dbl_count; ++i) doubles[i] = rand() / (RAND_MAX / 100.0);
  quicksort(doubles, doubles + dbl_count, sizeof(char*), &cmp_doubles_less);
  for (int i = 0; i < dbl_count; ++i) printf("%20.16f\n", doubles[i]);

  return EXIT_SUCCESS;
}

编辑:

仅供参考 Compiler Explorer 报告了非常明显的以下替代通用swap() 的程序集:

https://godbolt.org/z/GhvsY4

样本main()有:

int main() {
  int two = 2;
  int three = 3;

  swap(&two, &three, sizeof(int));
  swap2(&two, &three, sizeof(int));

  return two - three;
}

下面的swap2() 的完整汇编程序,但值得注意编译器已内联swap2()不是 swap() 当然包含对@ 的进一步调用987654343@。这可能是一些(全部?)差异?

swap2:
        push    rbp
        mov     rbp, rsp
        push    r14
        mov     r14, rdi
        push    r13
        mov     r13, rsi
        push    r12
        push    rbx
        cmp     rdx, 4
        je      .L9
        mov     r12, rdx
        cmp     rdx, 8
        jne     .L7
        mov     rax, QWORD PTR [rdi]
        mov     rdx, QWORD PTR [rsi]
        mov     QWORD PTR [rdi], rdx
        mov     QWORD PTR [rsi], rax
        lea     rsp, [rbp-32]
        pop     rbx
        pop     r12
        pop     r13
        pop     r14
        pop     rbp
        ret
.L7:
        lea     rax, [rdx+15]
        mov     rbx, rsp
        mov     rsi, rdi
        and     rax, -16
        sub     rsp, rax
        mov     rdi, rsp
        call    memcpy
        mov     rdx, r12
        mov     rsi, r13
        mov     rdi, r14
        call    memcpy
        mov     rdx, r12
        mov     rsi, rsp
        mov     rdi, r13
        call    memcpy
        mov     rsp, rbx
        lea     rsp, [rbp-32]
        pop     rbx
        pop     r12
        pop     r13
        pop     r14
        pop     rbp
        ret
.L9:
        mov     eax, DWORD PTR [rdi]
        mov     edx, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], edx
        mov     DWORD PTR [rsi], eax
        lea     rsp, [rbp-32]
        pop     rbx
        pop     r12
        pop     r13
        pop     r14
        pop     rbp
        ret

【问题讨论】:

  • 所有等于 sizeof 其他类型的类型可能没有相同的对齐方式,它适用于字符文字和指针吗?
  • 正确。一个认真的实施将需要照顾到这一点。正如@WhozCraig 在 Andrew Henle 回答下的评论中指出的那样,这是可以解决的。它适用于我的架构 ( x86_64 ),适用于上述 main() 中的所有类型,包括 pointersdoubles,但对于跨平台实现,需要额外注意对齐。
  • 对于上面 main() 中的所有类型,它都适用于我的架构 ( x86_64 ) 问题是,你必须希望你的编译器不会做类似使用的事情用于实现您的代码的 SSE2 指令。 x86 上存在潜在的对齐问题,尽管太多的程序员没有意识到这些可能性。
  • @AndrewHenle 谢谢。我当然知道触及合规的边缘,更不用说便携了。可以做到合规吗?如果不能,那就解释了为什么我可以快 2 倍?如果可以,那么它引出了一个问题,为什么普通的memcpy 还没有做这样的事情?

标签: c generics swap memcpy


【解决方案1】:

这是否违反任何标准?

是的。

这是一个strict aliasing violation,可能违反6.3.2.3 Pointers, paragraph 7:“指向对象类型的指针可能会转换为指向不同对象类型的指针。如果生成的指针未正确对齐引用的类型,则行为是未定义。...”

【讨论】:

  • 好的,但这不违反通用void* qsort 的整个想法吗?
  • 请注意,这也可以通过构建一个更智能的交换机制来克服,该机制检测领先的不对齐内容,字节交换,然后为大部分内容拾取对齐。这对于内存移动算法并不少见(大多数体面的 memcpy 实现已经这样做了)。
  • @OliverSchönrock 不,同一部分的第 1 段提到 A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
  • @WhozCraig 是的,这正是我的期望!即,我怎样才能在不费力的情况下将现代的libc 击败 2 倍?我一定是错过了什么?
  • @OliverSchönrock:这是一个经典的权衡,你会一次又一次地遇到:通用库在大多数情况下都可以工作,但有时你需要专门化才能获得性能你需要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-05
  • 2014-11-25
  • 2011-08-06
  • 1970-01-01
  • 1970-01-01
  • 2011-05-18
相关资源
最近更新 更多