Optimisation des performances du débogueur rr : Adaptation matérielle et gestion de la mémoire

Défis de performance dans l'enregistrement d'exécution

Le débogueur rr est un outil essentiel pour l'enregistrement et la reproductibilité d'exécutions non déterministes. Cependant, l'instrumentation du code cible introduit inévitablement une surcharge. Pour maintenir une efficacité optimale, particulièrement lors du débogage d'applications multithreadées, l'architecture interne de rr repose sur des optimisations matérielles et logicielles pointues. Les princiapux axes d'amélioration concernent la minimisation de l'impact sur les compteurs de performance, l'ordonnancement précis des threads et l'optimisation des accès mémoire.

Adaptation algorithmique à l'architecture matérielle

L'efficacité de l'enregistrement des événements dépend de la capacité à interroger les compteurs de performance matériels (PMU). rr implémente une détection dynamique de la microarchitecture du processeur pour configurer les registres de manière optimale.

Détection de la microarchitecture

Le système définit une énumération stricte des architectures supportées, permettant de router les configurations vers les bons contrôleurs matériels :

enum class ProcessorArchitecture {
  Generic,
  IntelCore,
  IntelSkylake,
  AmdZen,
  ArmNeoverse,
  AppleSilicon
};

Chaque architecture possède un registre de configuration spécifique pour les événements de branchement :

struct PmuConfiguration {
  ProcessorArchitecture arch;
  const char* name;
  uint32_t branch_event_code;
  uint32_t extra_config;
  uint32_t skid_limit;
  uint32_t tick_threshold;
  uint32_t flags;
};

static const PmuConfiguration pmu_registry[] = {
  { ProcessorArchitecture::IntelSkylake, "Skylake", 0x5101c4, 0, 100, PMU_FLAG_RCB },
  { ProcessorArchitecture::AmdZen, "Zen", 0x5100d1, 0, 10000, PMU_FLAG_RCB | PMU_FLAG_UNBOUNDED },
  { ProcessorArchitecture::ArmNeoverse, "Neoverse N1", 0x21, 0x6F, 1000, PMU_FLAG_TAKEN },
  { ProcessorArchitecture::AppleSilicon, "M1 Firestorm", 0x90, 0, 1000, PMU_FLAG_TAKEN }
};

Stratégies de comptage de branches

Pour minimiser les interruptions matérielles, le débogueur alterne entre deux modes de comptage. Le mode PMU_FLAG_RCB cible excluisvement les branchements conditionnels retirés, tandis que le mode PMU_FLAG_TAKEN comptabilise l'ensemble des sauts d'exécution. Le choix dynamique entre ces deux modes réduit drastiquement la pénalité de performance sur les architectures modernes.

Exploitation du cache et gestion de la mémoire

Les opérations d'E/S et les accès mémoire non séquentiels sont des goulots d'étranglement majeurs. L'outil utilise des structures de données optimisées pour la localité spatiale et temporelle.

Segmentation mémoire efficace

Le suivi des plages mémoire du processus cible est assuré par une structure optimisée pour les calculs d'intersection, réduisant ainsi les copies inutiles :

class MemorySegment {
public:
  bool is_inside(const MemorySegment& target) const {
    return begin_addr <= target.begin_addr && target.limit_addr <= limit_addr;
  }

  bool has_overlap(const MemorySegment& other) const {
    void* max_start = std::max(begin_addr, other.begin_addr);
    void* min_end = std::min(limit_addr, other.limit_addr);
    return max_start < min_end;
  }

  MemorySegment compute_overlap(const MemorySegment& other) const {
    void* max_start = std::max(begin_addr, other.begin_addr);
    void* min_end = std::min(limit_addr, other.limit_addr);
    return MemorySegment(max_start, std::max(max_start, min_end));
  }

private:
  void* begin_addr;
  void* limit_addr;
};

Décompression par blocs et E/S

Les traces d'exécution sont compressées via l'algorithme Brotli. La lecture des flux compressés est conçue pour maximiser le taux de succès dans le cache L1/L2 du processeur en traitant les données par blocs de taille fixe :

bool StreamDecoder::load_next_chunk(size_t* bytes_to_skip) {
  while (true) {
    BlockHeader chunk_header;
    if (!read_full(file_descriptor, sizeof(chunk_header), &chunk_header, &current_offset)) {
      is_corrupted = true;
      return false;
    }

    std::vector<uint8_t> raw_payload;
    raw_payload.resize(chunk_header.payload_size);
    if (!read_full(file_descriptor, raw_payload.size(), raw_payload.data(), &current_offset)) {
      is_corrupted = true;
      return false;
    }

    decompressed_buffer.resize(chunk_header.original_size);
    read_position = 0;
    if (!inflate_data(raw_payload, decompressed_buffer)) {
      is_corrupted = true;
      return false;
    }
    return true;
  }
}

Ordonnancement dynamique des tâches

La reproductibilité stricte nécessite un contrôle précis de l'ordonnancement des threads, tout en évitant la dégradation des performances due à un changement de contexte excessif.

Ajustement probabiliste des priorités

Le planificateur applique des pénalités de priorité aléatoires pour forcer l'exploration de différents entrelacements, tout en favorisant le thread principal :

int TaskScheduler::assign_thread_priority(ExecutionTask* task) {
  double drop_chance = task->is_group_leader() ? primary_thread_drop_rate
                                               : secondary_thread_drop_rate;
  return generate_random_fraction() < drop_chance;
}

Configuration des quantums d'exécution

Pour simuler des conditions de concurrence réelles sans bloquer le débogueur, la durée des tranches de temps (time-slices) est modulée dynamiquement :

void TaskScheduler::configure_execution_quantum() {
  Ticks quantum_limit = max_allowed_ticks;
  if (chaos_mode_enabled) {
    double random_val = generate_random_fraction();
    if (random_val < micro_quantum_chance) {
      quantum_limit = micro_quantum_limit;
    } else if (random_val < micro_quantum_chance + short_quantum_chance) {
      quantum_limit = short_quantum_limit;
    } else {
      quantum_limit = max_allowed_ticks;
    }
  }
  quantum_end_tick = current_task->get_ticks() +
                     (generate_random() % std::min(max_allowed_ticks, quantum_limit));
}

Surveillance via les compteurs matériels

L'intégration native des API perf_event permet de profiler le débogueur lui-même et d'ajuster les seuils d'interruption en temps réel.

struct HardwareCounterConfig {
  int error_flags = 0;
  perf_event_attr tick_event{};
  perf_event_attr inverse_tick_event{};
  perf_event_attr cycle_event{};
  perf_event_attr llsc_failure_event{};
  const char *target_pmu = nullptr;
  uint32_t pmu_capabilities = 0;
  uint32_t skid_tolerance = 0;
  bool validated = false;
  bool has_period_bug = false;
  bool single_counter_only = false;
  bool requires_dummy_counter = false;
};

Pratiques d'ingénierie dérivées

  • Adaptation matérielle : Ne jamais supposer une topologie de processeur uniforme. Implémenter des registres de configuration spécifiques pour exploiter les compteurs PMU natifs.
  • Localité des données : Structurer les plages mémoire et les tampons d'E/S pour garantir que les accès séquentiels restent dans les lignes de cache du processeur.
  • Ordonnancement chaotique contrôlé : Utiliser des distributions de probabilité pour les quantums d'exécution afin d'exposer les conditions de course sans sacrifier la vélocité de l'enregistrement.

Étiquettes: rr-debugger C++ performance-tuning cpu-architecture memory-management

Publié le 24 juillet à 19h05