web-dev-qa-db-fra.com

Mesurer les latences du cache

J'essaie donc de mesurer les latences des caches L1, L2 et L3 à l'aide de C. Je connais leur taille et je sens que je comprends conceptuellement comment le faire, mais je rencontre des problèmes avec mon implémentation. Je me demande si certaines des subtilités matérielles, telles que le prélecture, posent problème.

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

int main(){
    srand(time(NULL));  // Seed ONCE
    const int L1_CACHE_SIZE =  32768/sizeof(int);
    const int L2_CACHE_SIZE =  262144/sizeof(int);
    const int L3_CACHE_SIZE =  6587392/sizeof(int);
    const int NUM_ACCESSES = 1000000;
    const int SECONDS_PER_NS = 1000000000;
    int arrayAccess[L1_CACHE_SIZE];
    int arrayInvalidateL1[L1_CACHE_SIZE];
    int arrayInvalidateL2[L2_CACHE_SIZE];
    int arrayInvalidateL3[L3_CACHE_SIZE];
    int count=0;
    int index=0;
    int i=0;
    struct timespec startAccess, endAccess;
    double mainMemAccess, L1Access, L2Access, L3Access;
    int readValue=0;

    memset(arrayAccess, 0, L1_CACHE_SIZE*sizeof(int));
    memset(arrayInvalidateL1, 0, L1_CACHE_SIZE*sizeof(int));
    memset(arrayInvalidateL2, 0, L2_CACHE_SIZE*sizeof(int));
    memset(arrayInvalidateL3, 0, L3_CACHE_SIZE*sizeof(int));

    index = 0;
    clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    while (index < L1_CACHE_SIZE) {
        int tmp = arrayAccess[index];               //Access Value from L2
        index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
        count++;                                           //divide overall time by this 
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
    mainMemAccess = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
    mainMemAccess /= count;

    printf("Main Memory Access %lf\n", mainMemAccess);

    index = 0;
    count=0;
    clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    while (index < L1_CACHE_SIZE) {
        int tmp = arrayAccess[index];               //Access Value from L2
        index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
        count++;                                           //divide overall time by this 
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock              
    L1Access = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
    L1Access /= count;

    printf("L1 Cache Access %lf\n", L1Access);

    //invalidate L1 by accessing all elements of array which is larger than cache
    for(count=0; count < L1_CACHE_SIZE; count++){
        int read = arrayInvalidateL1[count]; 
        read++;
        readValue+=read;               
    }

    index = 0;
    count = 0;
    clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    while (index < L1_CACHE_SIZE) {
        int tmp = arrayAccess[index];               //Access Value from L2
        index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
        count++;                                           //divide overall time by this 
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
    L2Access = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
    L2Access /= count;

    printf("L2 Cache Acces %lf\n", L2Access);

    //invalidate L2 by accessing all elements of array which is larger than cache
    for(count=0; count < L2_CACHE_SIZE; count++){
        int read = arrayInvalidateL2[count];  
        read++;
        readValue+=read;                        
    }

    index = 0;
    count=0;
    clock_gettime(CLOCK_REALTIME, &startAccess); //sreadValue+=read;tart clock
    while (index < L1_CACHE_SIZE) {
        int tmp = arrayAccess[index];               //Access Value from L2
        index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
        count++;                                           //divide overall time by this 
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
    L3Access = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
    L3Access /= count;

    printf("L3 Cache Access %lf\n", L3Access);

    printf("Read Value: %d", readValue);

}

Je commence par accéder à une valeur du tableau dans lequel je veux des données. Cela devrait évidemment provenir de la mémoire principale car c’est le premier accès. Le tableau est petit (moins que la taille de la page), il devrait donc être copié dans L1, L2, L3. J'accède à la valeur du même tableau qui devrait maintenant être L1. J'accède ensuite à toutes les valeurs d'un tableau de la même taille que le cache L1 pour invalider les données auxquelles je souhaite accéder (maintenant, il devrait simplement s'agir de L2/3). Ensuite, je répète ce processus pour L2 et L3. Les temps d'accès sont clairement définis, ce qui signifie que je fais quelque chose de mal ...

Je pense qu’il pourrait y avoir des problèmes avec le temps qu’il faut pour chronométrer (le démarrage et l’arrêt prendront un peu de temps en ns et cela changera quand ils seront mis en cache/non synchronisés)

Est-ce que quelqu'un peut me donner des indications sur ce que je peux mal faire?

UPDATE1: J'ai donc amorti le coût de la minuterie en effectuant de nombreux accès, j'ai fixé la taille de mes caches et pris également le conseil de créer un système d'indexation plus complexe pour éviter les pas fixes. Malheureusement, les temps ne sont pas encore atteints. Ils semblent tous venir pour la L1. Je pense que le problème pourrait être d’invalider au lieu d’accéder. Un schéma aléatoire vs LRU aurait-il une incidence sur l'invalidation des données?

UPDATE2: Correction du memset (Ajout du memset L3 pour invalider également les données en L3 afin que le premier accès commence par la mémoire principale) et du schéma d'indexation, toujours pas de chance.

UPDATE3: Je ne pouvais jamais obtenir cette méthode au travail mais il y avait quelques bonnes suggestions de réponses et j'ai posté quelques solutions par moi-même.

J'ai aussi couru Cachegrind pour voir hit/miss

 ==6710== I   refs:      1,735,104
==6710== I1  misses:        1,092
==6710== LLi misses:        1,084
==6710== I1  miss rate:      0.06%
==6710== LLi miss rate:      0.06%
==6710== 
==6710== D   refs:      1,250,696  (721,162 rd   + 529,534 wr)
==6710== D1  misses:      116,492  (  7,627 rd   + 108,865 wr)
==6710== LLd misses:      115,102  (  6,414 rd   + 108,688 wr)
==6710== D1  miss rate:       9.3% (    1.0%     +    20.5%  )
==6710== LLd miss rate:       9.2% (    0.8%     +    20.5%  )
==6710== 
==6710== LL refs:         117,584  (  8,719 rd   + 108,865 wr)
==6710== LL misses:       116,186  (  7,498 rd   + 108,688 wr)
==6710== LL miss rate:        3.8% (    0.3%     +    20.5%  )


        Ir I1mr ILmr      Dr  D1mr  DLmr     Dw D1mw DLmw 

      .    .    .       .     .     .      .    .    .  #include <time.h>
      .    .    .       .     .     .      .    .    .  #include <stdio.h>
      .    .    .       .     .     .      .    .    .  #include <string.h>
      .    .    .       .     .     .      .    .    .  
      6    0    0       0     0     0      2    0    0  int main(){
      5    1    1       0     0     0      2    0    0      srand(time(NULL));  // Seed ONCE
      1    0    0       0     0     0      1    0    0      const int L1_CACHE_SIZE =  32768/sizeof(int);
      1    0    0       0     0     0      1    0    0      const int L2_CACHE_SIZE =  262144/sizeof(int);
      1    0    0       0     0     0      1    0    0      const int L3_CACHE_SIZE =  6587392/sizeof(int);
      1    0    0       0     0     0      1    0    0      const int NUM_ACCESSES = 1000000;
      1    0    0       0     0     0      1    0    0      const int SECONDS_PER_NS = 1000000000;
     21    2    2       3     0     0      3    0    0      int arrayAccess[L1_CACHE_SIZE];
     21    1    1       3     0     0      3    0    0      int arrayInvalidateL1[L1_CACHE_SIZE];
     21    2    2       3     0     0      3    0    0      int arrayInvalidateL2[L2_CACHE_SIZE];
     21    1    1       3     0     0      3    0    0      int arrayInvalidateL3[L3_CACHE_SIZE];
      1    0    0       0     0     0      1    0    0      int count=0;
      1    1    1       0     0     0      1    0    0      int index=0;
      1    0    0       0     0     0      1    0    0      int i=0;
      .    .    .       .     .     .      .    .    .      struct timespec startAccess, endAccess;
      .    .    .       .     .     .      .    .    .      double mainMemAccess, L1Access, L2Access, L3Access;
      1    0    0       0     0     0      1    0    0      int readValue=0;
      .    .    .       .     .     .      .    .    .  
      7    0    0       2     0     0      1    1    1      memset(arrayAccess, 0, L1_CACHE_SIZE*sizeof(int));
      7    1    1       2     2     0      1    0    0      memset(arrayInvalidateL1, 0, L1_CACHE_SIZE*sizeof(int));
      7    0    0       2     2     0      1    0    0      memset(arrayInvalidateL2, 0, L2_CACHE_SIZE*sizeof(int));
      7    1    1       2     2     0      1    0    0      memset(arrayInvalidateL3, 0, L3_CACHE_SIZE*sizeof(int));
      .    .    .       .     .     .      .    .    .  
      1    0    0       0     0     0      1    1    1      index = 0;
      4    0    0       0     0     0      1    0    0      clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    772    1    1     514     0     0      0    0    0      while (index < L1_CACHE_SIZE) {
  1,280    1    1     768   257   257    256    0    0          int tmp = arrayAccess[index];               //Access Value from L2
  2,688    0    0     768     0     0    256    0    0          index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
    256    0    0     256     0     0      0    0    0          count++;                                           //divide overall time by this 
      .    .    .       .     .     .      .    .    .      }
      4    0    0       0     0     0      1    0    0      clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
     14    1    1       5     1     1      1    1    1      mainMemAccess = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
      6    0    0       2     0     0      1    0    0      mainMemAccess /= count;
      .    .    .       .     .     .      .    .    .  
      6    1    1       2     0     0      2    0    0      printf("Main Memory Access %lf\n", mainMemAccess);
      .    .    .       .     .     .      .    .    .  
      1    0    0       0     0     0      1    0    0      index = 0;
      1    0    0       0     0     0      1    0    0      count=0;
      4    1    1       0     0     0      1    0    0      clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    772    1    1     514     0     0      0    0    0      while (index < L1_CACHE_SIZE) {
  1,280    0    0     768   240     0    256    0    0          int tmp = arrayAccess[index];               //Access Value from L2
  2,688    0    0     768     0     0    256    0    0          index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
    256    0    0     256     0     0      0    0    0          count++;                                           //divide overall time by this 
      .    .    .       .     .     .      .    .    .      }
      4    0    0       0     0     0      1    0    0      clock_gettime(CLOCK_REALTIME, &endAccess); //end clock              
     14    1    1       5     0     0      1    1    0      L1Access = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
      6    1    1       2     0     0      1    0    0      L1Access /= count;
      .    .    .       .     .     .      .    .    .  
      6    0    0       2     0     0      2    0    0      printf("L1 Cache Access %lf\n", L1Access);
      .    .    .       .     .     .      .    .    .  
      .    .    .       .     .     .      .    .    .      //invalidate L1 by accessing all elements of array which is larger than cache
 32,773    1    1  24,578     0     0      1    0    0      for(count=0; count < L1_CACHE_SIZE; count++){
 40,960    0    0  24,576   513   513  8,192    0    0          int read = arrayInvalidateL1[count]; 
  8,192    0    0   8,192     0     0      0    0    0          read++;
 16,384    0    0  16,384     0     0      0    0    0          readValue+=read;               
      .    .    .       .     .     .      .    .    .      }
      .    .    .       .     .     .      .    .    .  
      1    0    0       0     0     0      1    0    0      index = 0;
      1    1    1       0     0     0      1    0    0      count = 0;
      4    0    0       0     0     0      1    1    0      clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    772    1    1     514     0     0      0    0    0      while (index < L1_CACHE_SIZE) {
  1,280    0    0     768   256     0    256    0    0          int tmp = arrayAccess[index];               //Access Value from L2
  2,688    0    0     768     0     0    256    0    0          index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
    256    0    0     256     0     0      0    0    0          count++;                                           //divide overall time by this 
      .    .    .       .     .     .      .    .    .      }
      4    1    1       0     0     0      1    0    0      clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
     14    0    0       5     1     0      1    1    0      L2Access = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
      6    1    1       2     0     0      1    0    0      L2Access /= count;
      .    .    .       .     .     .      .    .    .  
      6    0    0       2     0     0      2    0    0      printf("L2 Cache Acces %lf\n", L2Access);
      .    .    .       .     .     .      .    .    .  
      .    .    .       .     .     .      .    .    .      //invalidate L2 by accessing all elements of array which is larger than cache
262,149    2    2 196,610     0     0      1    0    0      for(count=0; count < L2_CACHE_SIZE; count++){
327,680    0    0 196,608 4,097 4,095 65,536    0    0          int read = arrayInvalidateL2[count];  
 65,536    0    0  65,536     0     0      0    0    0          read++;
131,072    0    0 131,072     0     0      0    0    0          readValue+=read;                        
      .    .    .       .     .     .      .    .    .      }
      .    .    .       .     .     .      .    .    .  
      1    0    0       0     0     0      1    0    0      index = 0;
      1    0    0       0     0     0      1    0    0      count=0;
      4    0    0       0     0     0      1    1    0      clock_gettime(CLOCK_REALTIME, &startAccess); //sreadValue+=read;tart clock
    772    1    1     514     0     0      0    0    0      while (index < L1_CACHE_SIZE) {
  1,280    0    0     768   256     0    256    0    0          int tmp = arrayAccess[index];               //Access Value from L2
  2,688    0    0     768     0     0    256    0    0          index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
    256    0    0     256     0     0      0    0    0          count++;                                           //divide overall time by this 
      .    .    .       .     .     .      .    .    .      }
      4    0    0       0     0     0      1    0    0      clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
     14    1    1       5     1     0      1    1    0      L3Access = ((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec);
      6    0    0       2     0     0      1    0    0      L3Access /= count;
      .    .    .       .     .     .      .    .    .  
      6    1    1       2     0     0      2    0    0      printf("L3 Cache Access %lf\n", L3Access);
      .    .    .       .     .     .      .    .    .  
      6    0    0       1     0     0      1    0    0      printf("Read Value: %d", readValue);
      .    .    .       .     .     .      .    .    .  
      3    0    0       3     0     0      0    0    0  }
25
PandaRaid

Je préférerais plutôt utiliser l'horloge matérielle comme mesure. L'instruction rdtsc vous indiquera le nombre de cycles en cours depuis la mise sous tension de la CPU. En outre, il est préférable d’utiliser asm pour vous assurer que les mêmes instructions sont utilisées à la fois pour les séries mesurées et pour les séries sèches. En utilisant cela et quelques statistiques astucieuses, je l’ai fait il ya longtemps:

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>


int i386_cpuid_caches (size_t * data_caches) {
    int i;
    int num_data_caches = 0;
    for (i = 0; i < 32; i++) {

        // Variables to hold the contents of the 4 i386 legacy registers
        uint32_t eax, ebx, ecx, edx; 

        eax = 4; // get cache info
        ecx = i; // cache id

        asm (
            "cpuid" // call i386 cpuid instruction
            : "+a" (eax) // contains the cpuid command code, 4 for cache query
            , "=b" (ebx)
            , "+c" (ecx) // contains the cache id
            , "=d" (edx)
        ); // generates output in 4 registers eax, ebx, ecx and edx 

        // taken from http://download.intel.com/products/processor/manual/325462.pdf Vol. 2A 3-149
        int cache_type = eax & 0x1F; 

        if (cache_type == 0) // end of valid cache identifiers
            break;

        char * cache_type_string;
        switch (cache_type) {
            case 1: cache_type_string = "Data Cache"; break;
            case 2: cache_type_string = "Instruction Cache"; break;
            case 3: cache_type_string = "Unified Cache"; break;
            default: cache_type_string = "Unknown Type Cache"; break;
        }

        int cache_level = (eax >>= 5) & 0x7;

        int cache_is_self_initializing = (eax >>= 3) & 0x1; // does not need SW initialization
        int cache_is_fully_associative = (eax >>= 1) & 0x1;


        // taken from http://download.intel.com/products/processor/manual/325462.pdf 3-166 Vol. 2A
        // ebx contains 3 integers of 10, 10 and 12 bits respectively
        unsigned int cache_sets = ecx + 1;
        unsigned int cache_coherency_line_size = (ebx & 0xFFF) + 1;
        unsigned int cache_physical_line_partitions = ((ebx >>= 12) & 0x3FF) + 1;
        unsigned int cache_ways_of_associativity = ((ebx >>= 10) & 0x3FF) + 1;

        // Total cache size is the product
        size_t cache_total_size = cache_ways_of_associativity * cache_physical_line_partitions * cache_coherency_line_size * cache_sets;

        if (cache_type == 1 || cache_type == 3) {
            data_caches[num_data_caches++] = cache_total_size;
        }

        printf(
            "Cache ID %d:\n"
            "- Level: %d\n"
            "- Type: %s\n"
            "- Sets: %d\n"
            "- System Coherency Line Size: %d bytes\n"
            "- Physical Line partitions: %d\n"
            "- Ways of associativity: %d\n"
            "- Total Size: %zu bytes (%zu kb)\n"
            "- Is fully associative: %s\n"
            "- Is Self Initializing: %s\n"
            "\n"
            , i
            , cache_level
            , cache_type_string
            , cache_sets
            , cache_coherency_line_size
            , cache_physical_line_partitions
            , cache_ways_of_associativity
            , cache_total_size, cache_total_size >> 10
            , cache_is_fully_associative ? "true" : "false"
            , cache_is_self_initializing ? "true" : "false"
        );
    }

    return num_data_caches;
}

int test_cache(size_t attempts, size_t lower_cache_size, int * latencies, size_t max_latency) {
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd < 0) {
        perror("open");
        abort();
    }
    char * random_data = mmap(
          NULL
        , lower_cache_size
        , PROT_READ | PROT_WRITE
        , MAP_PRIVATE | MAP_ANON // | MAP_POPULATE
        , -1
        , 0
        ); // get some random data
    if (random_data == MAP_FAILED) {
        perror("mmap");
        abort();
    }

    size_t i;
    for (i = 0; i < lower_cache_size; i += sysconf(_SC_PAGESIZE)) {
        random_data[i] = 1;
    }


    int64_t random_offset = 0;
    while (attempts--) {
        // use processor clock timer for exact measurement
        random_offset += Rand();
        random_offset %= lower_cache_size;
        int32_t cycles_used, edx, temp1, temp2;
        asm (
            "mfence\n\t"        // memory fence
            "rdtsc\n\t"         // get cpu cycle count
            "mov %%edx, %2\n\t"
            "mov %%eax, %3\n\t"
            "mfence\n\t"        // memory fence
            "mov %4, %%al\n\t"  // load data
            "mfence\n\t"
            "rdtsc\n\t"
            "sub %2, %%edx\n\t" // substract cycle count
            "sbb %3, %%eax"     // substract cycle count
            : "=a" (cycles_used)
            , "=d" (edx)
            , "=r" (temp1)
            , "=r" (temp2)
            : "m" (random_data[random_offset])
            );
        // printf("%d\n", cycles_used);
        if (cycles_used < max_latency)
            latencies[cycles_used]++;
        else 
            latencies[max_latency - 1]++;
    }

    munmap(random_data, lower_cache_size);

    return 0;
} 

int main() {
    size_t cache_sizes[32];
    int num_data_caches = i386_cpuid_caches(cache_sizes);

    int latencies[0x400];
    memset(latencies, 0, sizeof(latencies));

    int empty_cycles = 0;

    int i;
    int attempts = 1000000;
    for (i = 0; i < attempts; i++) { // measure how much overhead we have for counting cyscles
        int32_t cycles_used, edx, temp1, temp2;
        asm (
            "mfence\n\t"        // memory fence
            "rdtsc\n\t"         // get cpu cycle count
            "mov %%edx, %2\n\t"
            "mov %%eax, %3\n\t"
            "mfence\n\t"        // memory fence
            "mfence\n\t"
            "rdtsc\n\t"
            "sub %2, %%edx\n\t" // substract cycle count
            "sbb %3, %%eax"     // substract cycle count
            : "=a" (cycles_used)
            , "=d" (edx)
            , "=r" (temp1)
            , "=r" (temp2)
            :
            );
        if (cycles_used < sizeof(latencies) / sizeof(*latencies))
            latencies[cycles_used]++;
        else 
            latencies[sizeof(latencies) / sizeof(*latencies) - 1]++;

    }

    {
        int j;
        size_t sum = 0;
        for (j = 0; j < sizeof(latencies) / sizeof(*latencies); j++) {
            sum += latencies[j];
        }
        size_t sum2 = 0;
        for (j = 0; j < sizeof(latencies) / sizeof(*latencies); j++) {
            sum2 += latencies[j];
            if (sum2 >= sum * .75) {
                empty_cycles = j;
                fprintf(stderr, "Empty counting takes %d cycles\n", empty_cycles);
                break;
            }
        }
    }

    for (i = 0; i < num_data_caches; i++) {
        test_cache(attempts, cache_sizes[i] * 4, latencies, sizeof(latencies) / sizeof(*latencies));

        int j;
        size_t sum = 0;
        for (j = 0; j < sizeof(latencies) / sizeof(*latencies); j++) {
            sum += latencies[j];
        }
        size_t sum2 = 0;
        for (j = 0; j < sizeof(latencies) / sizeof(*latencies); j++) {
            sum2 += latencies[j];
            if (sum2 >= sum * .75) {
                fprintf(stderr, "Cache ID %i has latency %d cycles\n", i, j - empty_cycles);
                break;
            }
        }

    }

    return 0;

}

Sortie sur mon Core2Duo:

Cache ID 0:
- Level: 1
- Type: Data Cache
- Total Size: 32768 bytes (32 kb)

Cache ID 1:
- Level: 1
- Type: Instruction Cache
- Total Size: 32768 bytes (32 kb)

Cache ID 2:
- Level: 2
- Type: Unified Cache
- Total Size: 262144 bytes (256 kb)

Cache ID 3:
- Level: 3
- Type: Unified Cache
- Total Size: 3145728 bytes (3072 kb)

Empty counting takes 90 cycles
Cache ID 0 has latency 6 cycles
Cache ID 2 has latency 21 cycles
Cache ID 3 has latency 168 cycles
25
Sergey L.

Ok, plusieurs problèmes avec votre code:

  1. Comme vous l'avez mentionné, vos mesures prennent beaucoup de temps. En fait, ils risquent de prendre beaucoup plus de temps que l’accès unique lui-même, ils ne mesurent donc rien d’utile. Pour limiter cela, accédez à plusieurs éléments et amortissez (divisez le temps total par le nombre d'accès. Notez que pour mesurer la latence, vous souhaitez que ces accès soient sérialisés, sinon ils peuvent être exécutés en parallèle et vous ne mesurerez que le débit. Pour y parvenir, vous pouvez simplement ajouter une fausse dépendance entre les accès. 

    Par exemple, initialisez le tableau à zéro, et faites: 

    clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    for (int i = 0; i < NUM_ACCESSES; ++i) {
        int tmp = arrayAccess[index];                             //Access Value from Main Memory
        index = (index + i + tmp) & 1023;   
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
    

    .. et bien sûr, n'oubliez pas de diviser l'heure par NUM_ACCESSES.
    Maintenant, j’ai volontairement compliqué l’index afin d’éviter une foulée fixe qui pourrait déclencher un prefetcher (un peu exagéré, vous ne remarquerez probablement pas d’impact, mais pour des raisons de démonstration ...) Vous pourriez probablement vous contenter d'un index += 32 simple, ce qui vous donnerait des foulées de 128 k (deux lignes de cache), et vous éviterait le "bénéfice" de la plupart des prefetchers simples de lignes/flux adjacents. J'ai également remplacé le % 1000 par le & 1023 car le & est plus rapide, mais il faut que la puissance 2 soit identique - il suffit donc d'augmenter le ACCESS_SIZE à 1024 et cela devrait fonctionner.

  2. Invalider la L1 en chargeant quelque chose d’autre est bon, mais les tailles ont l’air drôle. Vous n'avez pas spécifié votre système mais 256000 semble plutôt gros pour un L1. Un niveau 2 correspond généralement à 256 Ko sur de nombreux processeurs x86 modernes courants, par exemple. Notez également que 256k est pas 256000, mais plutôt 256*1024=262144. Il en va de même pour la deuxième taille: 1M n'est pas 1024000, c'est 1024*1024=1048576. En supposant que ce soit effectivement votre taille L2 (plus probablement un L3, mais probablement trop petit pour cela).

  3. Vos tableaux invalidants sont de type int, ainsi chaque élément est plus long qu'un seul octet (probablement 4 octets, selon le système). En fait, vous invalidez une valeur d'octets de L1_CACHE_SIZE*sizeof(int) (il en va de même pour la boucle d'invalidation L2)

Mettre à jour:

  1. memset reçoit la taille en octets, vos tailles sont divisées par sizeof(int)

  2. Vos lectures d'invalidation ne sont jamais utilisées et peuvent être optimisées. Essayez d'accumuler les lectures dans une valeur et de les imprimer à la fin, pour éviter cette possibilité.

  3. Le memset au début accède également aux données. Par conséquent, votre première boucle accède aux données du L3 (car les 2 autres memsets étaient toujours efficaces pour l’exclure de L1 + L2, bien que partiellement à cause de l’erreur de taille.

  4. Les pas peuvent être trop petits pour que vous ayez deux accès à la même cacheline (N1 touché). Assurez-vous qu'ils sont suffisamment dispersés en ajoutant 32 éléments (x4 octets) - c'est-à-dire 2 cacheline - vous n'obtiendrez donc aucun avantage en prélecture de cacheline adjacente.

  5. Étant donné que NUM_ACCESSES est supérieur à ACCESS_SIZE, vous répétez essentiellement les mêmes éléments et obtiendrez probablement des hits L1 pour eux (le temps moyen se déplace donc en faveur de la latence d'accès L1). Au lieu de cela, essayez d’utiliser la taille L1 pour pouvoir accéder à la totalité de la L1 (à l’exception des sauts) une seule fois. Par exemple comme ça - 

    index = 0;
    while (index < L1_CACHE_SIZE) {
        int tmp = arrayAccess[index];               //Access Value from L2
        index = (index + tmp + ((index & 4) ? 28 : 36));   // on average this should give 32 element skips, with changing strides
        count++;                                           //divide overall time by this 
    }
    

n'oubliez pas d'augmenter arrayAccess à la taille L1.

Maintenant, avec les changements ci-dessus (plus ou moins), j'obtiens quelque chose comme ceci:

L1 Cache Access 7.812500
L2 Cache Acces 15.625000
L3 Cache Access 23.437500

Ce qui semble encore un peu long, mais peut-être parce qu'il inclut une dépendance supplémentaire aux opérations arithmétiques 

7
Leeor

Le test classique largement utilisé pour la latence du cache parcourt la liste liée. Il fonctionne sur les processeurs superscalar/superpipelined modernes et même sur les cœurs hors service tels que ARM Cortex-A9 + et Intel Core 2/ix. Cette méthode est utilisée par open-source lmbench - dans le test lat_mem_rd ( page de manuel ) et dans l'outil de mesure du temps de latence de CPU-Z: http://cpuid.com/medias/files/softwares/misc/latency .Zip (binaire Windows natif)

Il existe des sources de test lat_mem_rd à partir de lmbench: https://github.com/foss-for-synopsys-dwc-arc-processors/lmbench/blob/master/src/lat_mem_rd.c

Et le test principal est

#define ONE p = (char **)*p;
#define FIVE    ONE ONE ONE ONE ONE
#define TEN FIVE FIVE
#define FIFTY   TEN TEN TEN TEN TEN
#define HUNDRED FIFTY FIFTY

void
benchmark_loads(iter_t iterations, void *cookie)
{
    struct mem_state* state = (struct mem_state*)cookie;
    register char **p = (char**)state->p[0];
    register size_t i;
    register size_t count = state->len / (state->line * 100) + 1;

    while (iterations-- > 0) {
        for (i = 0; i < count; ++i) {
            HUNDRED;
        }
    }

    use_pointer((void *)p);
    state->p[0] = (char*)p;
}

Donc, après avoir déchiffré la macro, nous effectuons beaucoup d'opérations linéaires comme:

 p = (char**) *p;  // (in intel syntax) == mov eax, [eax]
 p = (char**) *p;
 p = (char**) *p;
 ....   // 100 times total
 p = (char**) *p;

sur la mémoire, rempli de pointeurs, chaque élément stride pointant vers l’avant.

Comme le dit la page de manuel http://www.bitmover.com/lmbench/lat_mem_rd.8.html

L'indice de référence s'exécute sous forme de deux boucles imbriquées. La boucle externe est la taille de la foulée. La boucle interne est la taille du tableau. Pour chaque taille de tableau, le point de repère crée un anneau de pointeurs qui pointe d'un pas. La traversée du tableau se fait par

 p = (char **)*p;

dans une boucle for (la tête de la boucle for n'est pas significative; la boucle est une boucle déroulée longue de 1000 charges). La boucle s'arrête après un million de chargements. La taille du tableau varie de 512 octets à (généralement) huit mégaoctets. Pour les petites tailles, le cache aura un effet et les chargements seront beaucoup plus rapides. Cela devient beaucoup plus évident lorsque les données sont tracées.

Une description plus détaillée avec des exemples sur les POWER est disponible sur le wiki d'IBM: Démêler les mesures d'accès à la mémoire - lat_mem_rd - par Jenifer Hopper 2013

Le test lat_mem_rd ( http://www.bitmover.com/lmbench/lat_mem_rd.8.html ) prend deux arguments, une taille de tableau en Mo et une taille de foulée. Le benchmark utilise deux boucles pour parcourir le tableau, en utilisant la foulée comme incrément en créant un anneau de pointeurs qui la dirigent vers l’avant. Le test mesure la latence de lecture de la mémoire en nanosecondes pour la plage de tailles de mémoire. La sortie comprend deux colonnes: la première est la taille du tableau en Mo (la valeur en virgule flottante) et la seconde est la latence de chargement sur tous les points du tableau. Lorsque les résultats sont représentés graphiquement, vous pouvez voir clairement les latences relatives de toute la hiérarchie de la mémoire, y compris la latence plus rapide de chaque niveau de cache et la latence de la mémoire principale. 

PS: Il existe un document d’Intel (merci à Eldar Abusalimov ) avec des exemples d’exécution de lat_mem_rd: ftp://download.intel.com/design/intarch/PAPERS/321074.pdf - désolé right url is http://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-cache-latency-bandwidth-paper.pdf " Mesure de la latence du cache et de la mémoire et de la bande passante entre mémoire - Pour utilisation avec Intel Architecture "par Joshua Ruggiero à partir de décembre 2008:

6
osgx

Pas vraiment une réponse, mais lisez quand même quelque chose a déjà été mentionné dans d'autres réponses et commentaires ici

eh bien, l’autre jour, je réponds à cette question:

il s’agit de mesurer le taux de transfert de L1/L2/.../L?/MEMORY. Regardez-le pour un meilleur point de départ de votre problème.

[Notes]

  1. Je recommande fortement d'utiliser l'instruction RDTSC pour la mesure du temps

    en particulier pour L1 car tout le reste est trop lent. N'oubliez pas de définir l'affinité de processus sur singleCPUcar tous les cœurs ont leur propre compteur et leur nombre diffère beaucoup, même sur la même entrée Clock !!!

    Réglez le paramètreCPUclock sur Maximum pour les ordinateurs à horloge variable et n'oubliez pas de comptabiliser le dépassement RDTSC si vous utilisez uniquement une partie 32 bits (compteur 32 bits de dépassement de processeur en une seconde). Pour le calcul du temps, utilisez l’horloge du processeur (mesurez-la ou utilisez la valeur de registre).

    t0 <- RDTSC
    Sleep(250);
    t1 <- RDTSC
    CPU f=(t1-t0)<<2 [Hz]
    
  2. définit l'affinité de processus sur un seul processeur

    tousCPUles cœurs ont généralement leur propre L1, L2 caches donc sur plusieurs tâchesOSvous pouvez mesurer des choses confuses si vous ne le faites pas

  3. fait une sortie graphique (diagramme)

    alors vous verrez ce qui se passe réellement dans ce lien ci-dessus, j'ai posté pas mal de parcelles

  4. utilise la plus haute priorité de processus disponible par le système d'exploitation

1
Spektre

Pour les personnes intéressées, je ne pouvais pas mettre mon premier code au travail, alors j’ai essayé plusieurs approches alternatives qui ont donné des résultats décents.

La première utilisait des listes chaînées avec des nœuds alloués octets stride dans un espace mémoire contigu. Le déréférencement des noeuds atténue l'efficacité du pré-récupérateur et dans le cas où plusieurs lignes de cache sont extraites, j'ai effectué des pas de plus en plus importants pour éviter les accès en cache. Au fur et à mesure que la taille de la liste allouée augmente, il passe à la structure de mémoire cache ou de mémoire qui le tiendra en affichant des divisions nettes en temps de latence.

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

//MACROS
#define ONE iterate = (char**) *iterate;
#define FIVE ONE ONE ONE
#define TWOFIVE FIVE FIVE FIVE FIVE FIVE
#define HUNDO TWOFIVE TWOFIVE TWOFIVE TWOFIVE

//prototype
void allocateRandomArray(long double);
void accessArray(char *, long double, char**);

int main(){
    //call the function for allocating arrays of increasing size in MB
    allocateRandomArray(.00049);
    allocateRandomArray(.00098);
    allocateRandomArray(.00195);
    allocateRandomArray(.00293);
    allocateRandomArray(.00391);
    allocateRandomArray(.00586);
    allocateRandomArray(.00781);
    allocateRandomArray(.01172);
    allocateRandomArray(.01562);
    allocateRandomArray(.02344);
    allocateRandomArray(.03125);
    allocateRandomArray(.04688);
    allocateRandomArray(.0625);
    allocateRandomArray(.09375);
    allocateRandomArray(.125);
    allocateRandomArray(.1875);
    allocateRandomArray(.25);
    allocateRandomArray(.375);
    allocateRandomArray(.5);
    allocateRandomArray(.75);
    allocateRandomArray(1);
    allocateRandomArray(1.5);
    allocateRandomArray(2);
    allocateRandomArray(3);
    allocateRandomArray(4);
    allocateRandomArray(6);
    allocateRandomArray(8);
    allocateRandomArray(12);
    allocateRandomArray(16);
    allocateRandomArray(24);
    allocateRandomArray(32);
    allocateRandomArray(48);
    allocateRandomArray(64);
    allocateRandomArray(96);
    allocateRandomArray(128);
    allocateRandomArray(192);
}

void allocateRandomArray(long double size){
    int accessSize=(1024*1024*size); //array size in bytes
    char * randomArray = malloc(accessSize*sizeof(char));    //allocate array of size allocate size
    int counter;
    int strideSize=4096;        //step size

    char ** head = (char **) randomArray;   //start of linked list in contiguous memory
    char ** iterate = head;         //iterator for linked list
    for(counter=0; counter < accessSize; counter+=strideSize){      
        (*iterate) = &randomArray[counter+strideSize];      //iterate through linked list, having each one point stride bytes forward
        iterate+=(strideSize/sizeof(iterate));          //increment iterator stride bytes forward
    }
    *iterate = (char *) head;       //set tailf to point to head

    accessArray(randomArray, size, head);
    free(randomArray);
}

void accessArray(char *cacheArray, long double size, char** head){
    const long double NUM_ACCESSES = 1000000000/100;    //number of accesses to linked list
    const int SECONDS_PER_NS = 1000000000;      //const for timer
    FILE *fp =  fopen("accessData.txt", "a");   //open file for writing data
    int newIndex=0;
    int counter=0;
    int read=0;
    struct timespec startAccess, endAccess;     //struct for timer
    long double accessTime = 0;
    char ** iterate = head;     //create iterator

    clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    for(counter=0; counter < NUM_ACCESSES; counter++){
        HUNDO       //macro subsitute 100 accesses to mitigate loop overhead
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
    //calculate the time elapsed in ns per access
    accessTime = (((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec)) / (100*NUM_ACCESSES);
    fprintf(fp, "%Lf\t%Lf\n", accessTime, size);  //print results to file
    fclose(fp);  //close file
}

Cela a donné les résultats les plus cohérents et l'utilisation d'une variété de tailles de tableau et le traçage des latences respectives ont permis de distinguer très clairement les différentes tailles de cache présentes.

La méthode suivante, comme les tableaux de taille croissante alloués précédemment. Mais au lieu d'utiliser une liste chaînée pour accéder à la mémoire, je remplis chaque index avec son numéro respectif et je mélange de manière aléatoire le tableau. J'ai ensuite utilisé ces index pour parcourir les éléments de manière aléatoire dans le tableau, ce qui atténuait les effets du pré-extraction. Cependant, il y avait parfois un écart important dans le temps d'accès lorsque plusieurs lignes de cache adjacentes sont extraites et touchées.

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

//prototype
void allocateRandomArray(long double);
void accessArray(int *, long int);

int main(){
    srand(time(NULL));  // Seed random function
    int i=0;
    for(i=2; i < 32; i++){
        allocateRandomArray(pow(2, i));         //call latency function on arrays of increasing size
    }


}

void allocateRandomArray(long double size){
    int accessSize = (size) / sizeof(int);
    int * randomArray = malloc(accessSize*sizeof(int));
    int counter;

    for(counter=0; counter < accessSize; counter ++){
        randomArray[counter] = counter; 
    }
    for(counter=0; counter < accessSize; counter ++){
        int i,j;
        int swap;
        i = Rand() % accessSize;
        j = Rand() % accessSize;
        swap = randomArray[i];
        randomArray[i] = randomArray[j];
        randomArray[j] = swap;
    } 

    accessArray(randomArray, accessSize);
    free(randomArray);
}

void accessArray(int *cacheArray, long int size){
    const long double NUM_ACCESSES = 1000000000;
    const int SECONDS_PER_NS = 1000000000;
    int newIndex=0;
    int counter=0;
    int read=0;
    struct timespec startAccess, endAccess;
    long double accessTime = 0;

    clock_gettime(CLOCK_REALTIME, &startAccess); //start clock
    for(counter = 0; counter < NUM_ACCESSES; counter++){
        newIndex=cacheArray[newIndex];
    }
    clock_gettime(CLOCK_REALTIME, &endAccess); //end clock
    //calculate the time elapsed in ns per access
    accessTime = (((endAccess.tv_sec - startAccess.tv_sec) * SECONDS_PER_NS) + (endAccess.tv_nsec - startAccess.tv_nsec)) / (NUM_ACCESSES);
    printf("Access time: %Lf for size %ld\n", accessTime, size);
} 

En moyenne sur de nombreux essais, cette méthode a également produit des résultats relativement précis. Le premier choix est certainement le meilleur des deux, mais il s’agit d’une approche alternative qui fonctionne également bien.

0
PandaRaid