首页  >  文章  >  后端开发  >  追求性能第三部分:C Force

追求性能第三部分:C Force

王林
王林原创
2024-08-06 01:10:021173浏览

The Quest for Performance Part III : C Force

在本系列的前两期中,我们考虑了 Perl 中浮动操作的性能,
Python 和 R 在一个玩具示例中计算函数 cos(sin(sqrt(x))),其中 x 是一个 非常大 50M 双精度浮点数数组。
将算术密集型部分委托给 C 的混合实现是性能最高的实现之一。在本期中,我们将稍微偏离主题,看看玩具示例的纯 C 代码实现的性能。
C 代码将提供有关内存局部性对于性能重要性的进一步见解(默认情况下,C 数组中的元素存储在内存中的顺序地址中,以及数字 API,例如 PDL 或 numpy 与此类容器的接口)相对于容器,
例如Perl 数组不将其值存储在内存中的连续地址中。最后但同样重要的是,C 代码实现将允许我们评估与低级编译器(在本例中为 gcc)的浮点运算相关的标志是否会影响性能。
这一点值得强调:普通人在“管道”安装或构建内联文件时完全依赖于编译器标志的选择。如果一个人不触及这些标志,那么人们就会幸福地不知道他们可能会错过什么,或者他们可能会避免的陷阱。
简陋的 C 文件 makefile 允许人们明确地进行此类性能评估。

下面完整列出了我们玩具示例的 C 代码。该代码相当不言自明,因此除了指出它包含

的四个函数之外,不会花时间解释
  • 昂贵函数的非顺序计算:所有三个浮点操作都使用一个线程在单个循环内进行
  • 昂贵函数的顺序计算:3 个浮点函数计算中的每一个都使用一个线程在单独的循环中进行
  • 非顺序 OpenMP 代码:非顺序代码的线程版本
  • 顺序 OpenMP 代码:顺序代码的线程化

在这种情况下,人们可能希望编译器足够聪明,能够识别平方根映射到汇编中的打包(矢量化)浮点操作,以便可以使用适当的 SIMD 指令对一个函数进行矢量化(请注意,我们做了不使用 OpenMP 代码的 simd 程序)。
也许矢量化带来的加速可以抵消重复访问(或不访问)相同内存位置所造成的性能损失。

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <omp.h>

// simulates a large array of random numbers
double*  simulate_array(int num_of_elements,int seed);
// OMP environment functions
void _set_openmp_schedule_from_env();
void _set_num_threads_from_env();



// functions to modify C arrays 
void map_c_array(double* array, int len);
void map_c_array_sequential(double* array, int len);
void map_C_array_using_OMP(double* array, int len);
void map_C_array_sequential_using_OMP(double* array, int len);

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <array_size>\n", argv[0]);
        return 1;
    }

    int array_size = atoi(argv[1]);
    // printf the array size
    printf("Array size: %d\n", array_size);
    double *array = simulate_array(array_size, 1234);

    // Set OMP environment
    _set_openmp_schedule_from_env();
    _set_num_threads_from_env();

    // Perform calculations and collect timing data
    double start_time, end_time, elapsed_time;
    // Non-Sequential calculation
    start_time = omp_get_wtime();
    map_c_array(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Non-sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_c_array_sequential(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    array = simulate_array(array_size, 1234);
    // Parallel calculation using OMP
    start_time = omp_get_wtime();
    map_C_array_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Parallel calculation using OMP time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation using OMP
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_C_array_sequential_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation using OMP time: %f seconds\n", elapsed_time);

    free(array);
    return 0;
}



/*
*******************************************************************************
* OMP environment functions
*******************************************************************************
*/
void _set_openmp_schedule_from_env() {
  char *schedule_env = getenv("OMP_SCHEDULE");
  printf("Schedule from env %s\n", getenv("OMP_SCHEDULE"));
  if (schedule_env != NULL) {
    char *kind_str = strtok(schedule_env, ",");
    char *chunk_size_str = strtok(NULL, ",");

    omp_sched_t kind;
    if (strcmp(kind_str, "static") == 0) {
      kind = omp_sched_static;
    } else if (strcmp(kind_str, "dynamic") == 0) {
      kind = omp_sched_dynamic;
    } else if (strcmp(kind_str, "guided") == 0) {
      kind = omp_sched_guided;
    } else {
      kind = omp_sched_auto;
    }
    int chunk_size = atoi(chunk_size_str);
    omp_set_schedule(kind, chunk_size);
  }
}

void _set_num_threads_from_env() {
  char *num = getenv("OMP_NUM_THREADS");
  printf("Number of threads = %s from within C\n", num);
  omp_set_num_threads(atoi(num));
}
/*
*******************************************************************************
* Functions that modify C arrays whose address is passed from Perl in C
*******************************************************************************
*/

double*  simulate_array(int num_of_elements, int seed) {
  srand(seed); // Seed the random number generator
  double *array = (double *)malloc(num_of_elements * sizeof(double));
  for (int i = 0; i < num_of_elements; i++) {
    array[i] =
        (double)rand() / RAND_MAX; // Generate a random double between 0 and 1
  }
  return array;
}

void map_c_array(double *array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = cos(sin(sqrt(array[i])));
  }
}

void map_c_array_sequential(double* array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = sqrt(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = sin(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = cos(array[i]);
  }
}

void map_C_array_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(sin(sqrt(array[i])));
    }
  }
}

void map_C_array_sequential_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sqrt(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sin(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(array[i]);
    }
  }
}

一个关键问题是使用快速浮动编译器标志(一种以速度换取代码准确性的技巧)是否会影响性能。
这是没有这个编译器标志的 makefile

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native  -Wall -std=gnu11 -fopenmp -fstrict-aliasing 
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_noffmath_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_noffmath_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_noffmath_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

这是带有此标志的:

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native -Wall -std=gnu11 -fopenmp -fstrict-aliasing -ffast-math
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

这是运行这两个程序的结果

  • 没有-ffast-math
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_noffmath_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 1.12 seconds
Sequential calculation time: 0.95 seconds
Parallel calculation using OMP time: 0.17 seconds
Sequential calculation using OMP time: 0.15 seconds
  • 使用 -ffast-math
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 0.27 seconds
Sequential calculation time: 0.28 seconds
Parallel calculation using OMP time: 0.05 seconds
Sequential calculation using OMP time: 0.06 seconds

请注意,可以在 Numba 代码中使用 fastmath,如下所示(默认为 fastmath=False):

@njit(nogil=True,fastmath=True)
def compute_inplace_with_numba(array):
    np.sqrt(array,array)
    np.sin(array,array)
    np.cos(array,array)

值得注意的几点:

  • -ffast-math 显着提高了性能(单线程和多线程代码大约提高了 300%),但它可能会生成错误的结果
  • Fastmath 在 Numba 中也适用,但应避免使用,其原因与在任何追求准确性的应用程序中应避免的原因相同
  • 顺序 C 单线程代码的性能类似于单线程 PDL 和 Numpy
  • 有点令人惊讶的是,当使用正确(非快速)数学时,顺序代码比非顺序代码快大约 20%。
  • 毫不奇怪,多线程代码比单线程代码更快:)
  • 我仍然无法解释 numbas 如何为这个相当简单的函数提供比 C 代码高 50% 的性能。

标题:“性能追求第三部分:C Force”

日付: 2024-07-07

このシリーズの前 2 回の記事では、Perl での浮動演算のパフォーマンスについて検討しました。
関数 cos(sin(sqrt(x))) を計算するおもちゃの例では、Python と R が使用されています。ここで、x は 50M の倍精度浮動小数点数の 非常に大きな 配列です。
算術集中部分を C に委任したハイブリッド実装は、最もパフォーマンスの高い実装の 1 つでした。今回は少し脱線して、おもちゃのサンプルの純粋な C コード実装のパフォーマンスを見ていきます。
C コードは、パフォーマンスに対するメモリの局所性の重要性について、コンテナと比較してさらに洞察を提供します (デフォルトでは、C 配列の要素はメモリ内の連続したアドレスに格納され、PDL やそのようなコンテナとの numpy インターフェイスなどの数値 API) 、
例えばメモリ内の連続したアドレスに値を格納しない Perl 配列。最後に、重要なことですが、C コードの実装により、低レベル コンパイラ (この場合は gcc) の浮動小数点演算に関連するフラグがパフォーマンスに影響を与えるかどうかを評価できるようになります。
この点は強調する価値があります。一般的な人間は、「インストール」を「パイプ」するとき、またはインライン ファイルをビルドするときのコンパイラ フラグの選択に完全に依存しています。これらのフラグに触れなければ、何が欠けているのか、あるいは避けている落とし穴があるのか​​、幸いにも気づかないことになるでしょう。
謙虚な C ファイル makefile を使用すると、このようなパフォーマンス評価を明示的に行うことができます。

おもちゃの例の C コード全体を以下に示します。このコードは一目瞭然なので、

に対する 4 つの関数が含まれていることを指摘する以外は説明に時間を費やすことはありません。
  • 高価な関数の非順次計算: 3 つの浮動小数点演算はすべて、1 つのスレッドを使用して単一のループ内で実行されます
  • 高価な関数の逐次計算: 3 つの浮動小数点関数の評価はそれぞれ、1 つのスレッドを使用して別個のループ内で行われます
  • ノンシーケンシャル OpenMP コード: ノンシーケンシャル コードのスレッド バージョン
  • シーケンシャル OpenMP コード: シーケンシャル コードのスレッド化

この場合、コンパイラーが、平方根がアセンブリ内のパックされた (ベクトル化された) 浮動小数点演算にマップされることを認識するのに十分賢いので、適切な SIMD 命令を使用して 1 つの関数をベクトル化できることを期待するかもしれません (実際に実行したことに注意してください) OpenMP コードには simd プログラムを使用しないでください)。
おそらく、ベクトル化による高速化により、同じメモリ位置に繰り返しアクセスする (またはしない) ことによるパフォーマンスの低下が相殺される可能性があります。

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <omp.h>

// simulates a large array of random numbers
double*  simulate_array(int num_of_elements,int seed);
// OMP environment functions
void _set_openmp_schedule_from_env();
void _set_num_threads_from_env();



// functions to modify C arrays 
void map_c_array(double* array, int len);
void map_c_array_sequential(double* array, int len);
void map_C_array_using_OMP(double* array, int len);
void map_C_array_sequential_using_OMP(double* array, int len);

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <array_size>\n", argv[0]);
        return 1;
    }

    int array_size = atoi(argv[1]);
    // printf the array size
    printf("Array size: %d\n", array_size);
    double *array = simulate_array(array_size, 1234);

    // Set OMP environment
    _set_openmp_schedule_from_env();
    _set_num_threads_from_env();

    // Perform calculations and collect timing data
    double start_time, end_time, elapsed_time;
    // Non-Sequential calculation
    start_time = omp_get_wtime();
    map_c_array(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Non-sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_c_array_sequential(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    array = simulate_array(array_size, 1234);
    // Parallel calculation using OMP
    start_time = omp_get_wtime();
    map_C_array_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Parallel calculation using OMP time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation using OMP
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_C_array_sequential_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation using OMP time: %f seconds\n", elapsed_time);

    free(array);
    return 0;
}



/*
*******************************************************************************
* OMP environment functions
*******************************************************************************
*/
void _set_openmp_schedule_from_env() {
  char *schedule_env = getenv("OMP_SCHEDULE");
  printf("Schedule from env %s\n", getenv("OMP_SCHEDULE"));
  if (schedule_env != NULL) {
    char *kind_str = strtok(schedule_env, ",");
    char *chunk_size_str = strtok(NULL, ",");

    omp_sched_t kind;
    if (strcmp(kind_str, "static") == 0) {
      kind = omp_sched_static;
    } else if (strcmp(kind_str, "dynamic") == 0) {
      kind = omp_sched_dynamic;
    } else if (strcmp(kind_str, "guided") == 0) {
      kind = omp_sched_guided;
    } else {
      kind = omp_sched_auto;
    }
    int chunk_size = atoi(chunk_size_str);
    omp_set_schedule(kind, chunk_size);
  }
}

void _set_num_threads_from_env() {
  char *num = getenv("OMP_NUM_THREADS");
  printf("Number of threads = %s from within C\n", num);
  omp_set_num_threads(atoi(num));
}
/*
*******************************************************************************
* Functions that modify C arrays whose address is passed from Perl in C
*******************************************************************************
*/

double*  simulate_array(int num_of_elements, int seed) {
  srand(seed); // Seed the random number generator
  double *array = (double *)malloc(num_of_elements * sizeof(double));
  for (int i = 0; i < num_of_elements; i++) {
    array[i] =
        (double)rand() / RAND_MAX; // Generate a random double between 0 and 1
  }
  return array;
}

void map_c_array(double *array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = cos(sin(sqrt(array[i])));
  }
}

void map_c_array_sequential(double* array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = sqrt(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = sin(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = cos(array[i]);
  }
}

void map_C_array_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(sin(sqrt(array[i])));
    }
  }
}

void map_C_array_sequential_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sqrt(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sin(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(array[i]);
    }
  }
}

重要な問題は、高速フローティング コンパイラ フラグの使用 (コードの精度と速度を引き換えにするトリック) がパフォーマンスに影響を与える可能性があるかどうかです。
このコンパイラ フラグのないメイクファイルは次のとおりです

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native  -Wall -std=gnu11 -fopenmp -fstrict-aliasing 
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_noffmath_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_noffmath_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_noffmath_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

そして、これがこのフラグを持つものです:

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native -Wall -std=gnu11 -fopenmp -fstrict-aliasing -ffast-math
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

これら 2 つのプログラムを実行した結果が次のとおりです

  • -ffast-math なし
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_noffmath_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 1.12 seconds
Sequential calculation time: 0.95 seconds
Parallel calculation using OMP time: 0.17 seconds
Sequential calculation using OMP time: 0.15 seconds
  • -ffast-math を使用する
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 0.27 seconds
Sequential calculation time: 0.28 seconds
Parallel calculation using OMP time: 0.05 seconds
Sequential calculation using OMP time: 0.06 seconds

次のように Numba コードで fastmath を使用できることに注意してください (デフォルトは fastmath=False)。

@njit(nogil=True,fastmath=True)
def compute_inplace_with_numba(array):
    np.sqrt(array,array)
    np.sin(array,array)
    np.cos(array,array)

注目に値するいくつかの点:

  • -ffast-math はパフォーマンスを大幅に向上させます (シングルスレッドコードとマルチスレッドコードの両方で約 300%) が、誤った結果が生成される可能性があります
  • Fastmath は Numba でも動作しますが、正確さを追求するアプリケーションで避けるべきと同じ理由で避けるべきです
  • シーケンシャル C シングル スレッド コードは、シングル スレッド PDL や Numpy と同様のパフォーマンスを提供します
  • 少し驚くべきことに、正しい (高速ではない) 計算を使用すると、シーケンシャル コードは非シーケンシャル コードよりも約 20% 高速になります。
  • 当然のことながら、マルチスレッド コードはシングル スレッド コードより高速です :)
  • numbas が、このかなり単純な関数の C コードよりもどのようにパフォーマンスを 50% 向上させるのか、まだ説明できません。

以上是追求性能第三部分:C Force的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn