Pencarian untuk Persembahan Bahagian III : C Force

王林
Lepaskan: 2024-08-06 01:10:02
asal
1199 orang telah melayarinya

The Quest for Performance Part III : C Force

Dalam dua ansuran sebelum siri ini, kami mempertimbangkan prestasi operasi terapung di Perl,
Python dan R dalam contoh mainan yang mengira fungsi cos(sin(sqrt(x))), dengan x ialah tatasusunan sangat besar bagi nombor terapung berketepatan ganda 50M.
Pelaksanaan hibrid yang mewakilkan bahagian intensif aritmetik kepada C adalah antara pelaksanaan yang paling berprestasi. Dalam ansuran ini, kami akan menyimpang sedikit dan melihat prestasi pelaksanaan kod C tulen bagi contoh mainan.
Kod C akan memberikan pandangan lanjut tentang kepentingan lokaliti memori untuk prestasi (secara lalai elemen dalam tatasusunan C disimpan dalam alamat berurutan dalam ingatan, dan API berangka seperti PDL atau antara muka numpy dengan bekas tersebut) berbanding bekas ,
cth. Tatasusunan Perl yang tidak menyimpan nilainya dalam alamat berjujukan dalam ingatan. Akhir sekali, tetapi yang paling penting, pelaksanaan kod C akan membolehkan kami menilai sama ada bendera yang berkaitan dengan operasi titik terapung untuk pengkompil tahap rendah (dalam kes ini gcc) boleh menjejaskan prestasi.
Perkara ini patut ditekankan: manusia biasa bergantung sepenuhnya pada pilihan bendera pengkompil apabila "memasang" "memasang" mereka atau membina fail Sebaris mereka. Jika seseorang tidak menyentuh bendera ini, maka dia akan berasa gembira tidak menyedari apa yang mereka mungkin hilang, atau perangkap yang mungkin mereka elakkan.
Makefile fail C yang sederhana membolehkan seseorang membuat penilaian prestasi sedemikian secara eksplisit.

Kod C untuk contoh mainan kami disenaraikan secara keseluruhannya di bawah. Kod ini agak jelas, jadi tidak akan menghabiskan masa untuk menerangkan selain menunjukkan bahawa ia mengandungi empat fungsi untuk

  • Pengiraan bukan urutan bagi fungsi mahal : ketiga-tiga operasi menuding terapung berlaku di dalam satu gelung menggunakan satu benang
  • Pengiraan berurutan bagi fungsi mahal : setiap satu daripada 3 penilaian fungsi titik terapung mengambil dalam gelung berasingan menggunakan satu benang
  • Kod OpenMP bukan urutan : versi berulir kod bukan urutan
  • Kod OpenMP Berjujukan: berulir kod berjujukan

Dalam kes ini, seseorang mungkin berharap bahawa pengkompil cukup bijak untuk mengenali bahawa punca kuasa dua memetakan kepada operasi penunjuk terapung yang dibungkus (divektorkan) dalam pemasangan, supaya satu fungsi boleh divektorkan menggunakan arahan SIMD yang sesuai (nota yang kami lakukan tidak menggunakan program simd untuk kod OpenMP).
Mungkin kelajuan daripada pengvektoran mungkin mengimbangi kehilangan prestasi daripada berulang kali mengakses lokasi memori yang sama (atau tidak).

#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]);
    }
  }
}

Salin selepas log masuk
Salin selepas log masuk

Persoalan kritikal ialah sama ada penggunaan bendera pengkompil terapung cepat, helah yang menukar kelajuan untuk ketepatan kod, boleh menjejaskan prestasi.
Berikut ialah fail make tanpa bendera pengkompil ini

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 $@
Salin selepas log masuk
Salin selepas log masuk

dan inilah yang mempunyai bendera ini:

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 $@
Salin selepas log masuk
Salin selepas log masuk

Dan berikut adalah keputusan menjalankan kedua-dua program ini

  • Tanpa -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
Salin selepas log masuk
Salin selepas log masuk
  • Dengan -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
Salin selepas log masuk
Salin selepas log masuk

Perhatikan bahawa seseorang boleh menggunakan fastmath dalam kod Numba seperti berikut (lalai ialah 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)
Salin selepas log masuk
Salin selepas log masuk

Beberapa perkara yang patut diberi perhatian:

  • -ffast-math memberikan rangsangan besar dalam prestasi (kira-kira 300% untuk kedua-dua kod berulir tunggal dan berbilang benang), tetapi ia boleh menjana hasil yang salah
  • Fastmath juga berfungsi di Numba, tetapi harus dielakkan atas sebab yang sama ia harus dielakkan dalam mana-mana aplikasi yang berusaha untuk ketepatan
  • Kod berulir tunggal C berjujukan memberikan prestasi serupa dengan PDL berulir tunggal dan Numpy
  • Agak mengejutkan, kod jujukan adalah kira-kira 20% lebih pantas daripada kod bukan urutan apabila matematik yang betul (tidak pantas) digunakan.
  • Tidak menghairankan, kod berbilang benang lebih pantas daripada kod berbenang tunggal :)
  • Saya masih tidak dapat menerangkan cara numbas menyampaikan premium prestasi 50% berbanding kod C fungsi yang agak mudah ini.

tajuk: " Pencarian untuk Persembahan Bahagian III : C Force "

date: 2024-07-07

Dans les deux volets précédents de cette série, nous avons examiné les performances des opérations flottantes en Perl,
Python et R dans un exemple de jouet qui a calculé la fonction cos(sin(sqrt(x))), où x était un très grand tableau de 50 millions de nombres flottants double précision.
Les implémentations hybrides qui déléguaient la partie arithmétique intensive à C étaient parmi les implémentations les plus performantes. Dans cet article, nous allons nous éloigner légèrement et examiner les performances d'une implémentation en code C pur de l'exemple de jouet.
Le code C fournira des informations supplémentaires sur l'importance de la localité mémoire pour les performances (par défaut, les éléments d'un tableau C sont stockés dans des adresses séquentielles en mémoire, et les API numériques telles que PDL ou l'interface numpy avec de tels conteneurs) par rapport aux conteneurs. ,
par ex. Tableaux Perl qui ne stockent pas leurs valeurs dans des adresses séquentielles en mémoire. Dernier point, mais non le moindre, les implémentations du code C nous permettront d'évaluer si les indicateurs liés aux opérations en virgule flottante pour le compilateur de bas niveau (dans ce cas gcc) peuvent affecter les performances.
Ce point mérite d'être souligné : le commun des mortels dépend entièrement du choix des indicateurs du compilateur lors du "piping" de son "installation" ou de la construction de son fichier Inline. Si l’on ne touche pas ces drapeaux, on ignorera parfaitement ce qu’ils pourraient manquer ou les pièges qu’ils pourraient éviter.
Le modeste makefile du fichier C permet de faire explicitement de telles évaluations de performances.

Le code C de notre exemple de jouet est répertorié dans son intégralité ci-dessous. Le code est plutôt explicite, je ne passerai donc pas de temps à expliquer autre que souligner qu'il contient quatre fonctions pour

  • Calcul non séquentiel de la fonction coûteuse : les trois opérations de pointage flottant s'effectuent dans une seule boucle à l'aide d'un seul thread
  • Calculs séquentiels de la fonction coûteuse : chacune des 3 évaluations de fonctions à virgule flottante se déroule dans une boucle séparée à l'aide d'un seul thread
  • Code OpenMP non séquentiel : version threadée du code non séquentiel
  • Code OpenMP séquentiel : thread du code séquentiel

Dans ce cas, on peut espérer que le compilateur soit suffisamment intelligent pour reconnaître que la racine carrée correspond à des opérations de virgule flottante compressées (vectorisées) dans l'assembly, de sorte qu'une fonction puisse être vectorisée à l'aide des instructions SIMD appropriées (notez que nous l'avons fait n'utilisez pas le programme simd pour les codes OpenMP).
Peut-être que l'accélération de la vectorisation pourrait compenser la perte de performances liée à l'accès répété aux mêmes emplacements mémoire (ou non).

#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]);
    }
  }
}

Salin selepas log masuk
Salin selepas log masuk

Une question cruciale est de savoir si l'utilisation d'indicateurs de compilateur flottants rapides, une astuce qui échange la vitesse contre la précision du code, peut affecter les performances.
Voici le makefile sans ce drapeau du compilateur

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 $@
Salin selepas log masuk
Salin selepas log masuk

et voici celui avec ce drapeau :

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 $@
Salin selepas log masuk
Salin selepas log masuk

Et voici les résultats de l'exécution de ces deux programmes

  • Sans -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
Salin selepas log masuk
Salin selepas log masuk
  • Avec -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
Salin selepas log masuk
Salin selepas log masuk

Notez que l'on peut utiliser le fastmath dans le code Numba comme suit (la valeur par défaut est 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)
Salin selepas log masuk
Salin selepas log masuk

Quelques points à noter :

  • Le -ffast-math donne une amélioration majeure des performances (environ 300 % pour le code monothread et multithread), mais il peut générer des résultats erronés
  • Fastmath fonctionne également dans Numba, mais doit être évité pour les mêmes raisons qu'il doit être évité dans toute application qui recherche la précision
  • Le code séquentiel C monothread offre des performances similaires à celles du PDL et Numpy monothread
  • De manière quelque peu surprenante, le code séquentiel est environ 20 % plus rapide que le code non séquentiel lorsque les mathématiques correctes (non rapides) sont utilisées.
  • Sans surprise, le code multithread est plus rapide que le code monothread :)
  • Je n'arrive toujours pas à expliquer comment numbas offre une prime de performance de 50 % par rapport au code C de cette fonction plutôt simple.

Atas ialah kandungan terperinci Pencarian untuk Persembahan Bahagian III : C Force. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan