Retour à l'accueil

HyperLogLog et Count-Min Sketch en Go pour Big Data

L'article décrit les implémentations de HyperLogLog et Count-Min Sketch en Go pour travailler avec big data. Des exemples de code complets avec des tests sur des millions d'éléments, une comparaison de la mémoire et des performances sont fournis. Adapté au traitement de flux et aux systèmes distribués.

Algorithmes Big Data en Go : HLL et CMS avec des exemples de code
Advertisement 728x90

Structures de données probabilistes efficaces en Go : HyperLogLog et Count-Min Sketch

HyperLogLog estime le nombre d'éléments uniques dans un flux de données avec une erreur de 1 à 3 % en utilisant seulement 1 Ko de mémoire. L'algorithme hache les éléments, analyse le nombre de zéros initiaux dans leur représentation binaire et stocke les statistiques dans un tableau de registres. Cela fournit un temps d'insertion en O(1) et O(m) pour l'estimation, où m est le nombre de registres (généralement 1024).

Paramètres clés :

  • m = 1024 registres (8 bits par registre)
  • b = 8 bits pour l'indexation des registres
  • Fonctions de hachage : fnv64a avec mélange de bits
package main

import (
	"fmt"
	"hash/fnv"
	"math"
	"math/rand"
	"strconv"
)

const (
	m = 1024 // count registtrov. This osnovnoy parametr, vliyayuschiy on tochnost.
	b = 8  // number bit, ispolzuemykh for opredeleniya indexa register from hash.
)

type HyperLogLog struct {
    // when asynchronous interaction worth think about mutexes
	registers [m]byte
}

func NewHyperLogLog() *HyperLogLog {
	return &HyperLogLog{}
}

// hash returns dva 32-bitnykh numbers from stroki
func hash(s string) (uint32, uint32) {
	h := fnv.New64a()
	h.Write([]byte(s))
	v := h.Sum64()

	// Shuffling bits, so that uluchshit raspredelenie
	w := uint32(v >> 32)
	z := uint32(v)

	// Dobavlyaem peremeshivanie, uluchshaet kachestvo sluchaynosti and pomogaet izbezhat problem with plokhim raspredeleniem hashes
	w ^= z<<13 | z>>(32-13)
	z ^= w<<7 | w>>(32-7)
	w ^= z<<17 | z>>(32-17)

	return w, z
}

// Withchitaet count zeros left in dvoichnoy record numbers,
// if all bits — nuli, vozvraschaetsya 32.
func countLeadingZeros(x uint32) byte {
	for i := 0; i < 32; i++ {
		if (x>>(31-i))&1 == 1 {
			return byte(i)
		}
	}
	return 32
}

// Kheshiruem stroku.
// Beryom chast hash h1, so that define, in kakoy registtr popadyot element.
// By vtoroy chasti h2 schitaem, how many veduschikh zeros (rho), pribavlyaem 1.
// If eto wartość greater, than uzhe est in registtre — update ego.
func (hll *HyperLogLog) Add(s string) {
	h1, h2 := hash(s)

	// opredelyaem index register
	idx := h1 % m

	// vychislyaem wartość for update register
	rho := countLeadingZeros(h2) + 1

	if rho > hll.registers[idx] {
		hll.registers[idx] = rho
	}
}

// Withlozhnye formuly, by essence eta function
// delaet otsenku kolichestva unique elements
func (hll *HyperLogLog) Estimate() float64 {
	sum := 0.0
	for _, val := range hll.registers {
		sum += 1 / math.Pow(2, float64(val))
	}
	estimate := alpha(m) * m * m / sum

	// korrektsiya for malykh znacheniy, usessya metod
	// Linear Counting : than greater nulevykh registtrov — then
	// menshe realnaya kardinalnost.
	if estimate <= 5*m/2 {
		zeros := 0
		for _, val := range hll.registers {
			if val == 0 {
				zeros++
			}
		}
		if zeros != 0 {
			estimate = float64(m) * math.Log(float64(m)/float64(zeros))
		}
	}

	return estimate
}

// alpha — popravochnyy koeffitsient, kompensiruyuschiy systemticheskuyu oshibku
func alpha(m int) float64 {
	switch m {
	case 16:
		return 0.673
	case 32:
		return 0.697
	case 64:
		return 0.709
	default:
		return 0.7213 / (1 + 1.079/float64(m))
	}
}

func main() {
	// Withzdayom HLL.
	hll := NewHyperLogLog()

	seen := make(map[string]struct{})
	for i := 0; i < 1_000_000; i++ {
		key := strconv.Itoa(rand.Intn(1_000_000))
		seen[key] = struct{}{}
		hll.Add(key)
	}

  	nonZero := 0
	for _, r := range hll.registers {
		if r > 0 {
			nonZero++
		}
	}
  
	fmt.Printf("Realnoe count unique elements: %d\n", len(seen))
	fmt.Printf("Otsenka unique elements: %.2f\n", hll.Estimate())
	fmt.Printf("Zapolnennykh registtrov: %d/%d\n", nonZero, m)
}

Pour 1 million d'éléments uniques, l'erreur relative était de 0,84 %, avec 100 % des registres remplis. Mémoire : 1 Ko.

Google AdInline article slot

Count-Min Sketch pour le comptage de fréquences

Count-Min Sketch estime les fréquences des éléments dans un flux en utilisant un tableau 2D de compteurs (profondeur × largeur). Chaque ligne utilise une fonction de hachage séparée avec un sel. Lors de l'insertion, l'élément incrémente les compteurs dans toutes les lignes ; lors de la requête, on prend le minimum — cela minimise les effets de collision.

Paramètres d'implémentation :

  • largeur = 10 000 colonnes
  • profondeur = 5 lignes
  • Hachage : fnv64a avec mélange de graines

Méthodes de requête :

Google AdInline article slot
  • Minimum — basique, précis pour les mises à jour positives
  • Moyenne — robuste aux suppressions
  • Count-mean-min — corrige le biais
package main

import (
    "fmt"
    "hash/fnv"
    "math"
)

type CountMinSketch struct {
    width  int
    depth  int
    // when asynchronous interaction worth think about mutexes
    table  [][]int
    hashes []uint64
}

func NewCountMinSketch(width, depth int) *CountMinSketch {
    return &CountMinSketch{
        width:  width,
        depth:  depth,
        table:  make([][]int, depth),
        hashes: make([]uint64, depth),
    }
}

func (cms *CountMinSketch) Init() {
    for i := range cms.table {
        cms.table[i] = make([]int, cms.width)
        cms.hashes[i] = uint64(i+1) // prostye soli for raznykh hashes
    }
}

// getHashIndex generiruet khesh and returns index in table
func (cms *CountMinSketch) getHashIndex(s string, seed uint64) int {
    h := fnv.New64a()
    h.Write([]byte(s))
    v := h.Sum64()

    // Shuffling bits with seed
    mixed := v ^ seed ^ (seed << 32)

    return int(mixed % uint64(cms.width))
}

// Add uvelichivaet schetchik for element
func (cms *CountMinSketch) Add(s string, count int) {
    for i := 0; i < cms.depth; i++ {
        idx := cms.getHashIndex(s, cms.hashes[i])
        cms.table[i][idx] += count
    }
}

// Count returns otsenochnoe number vkhozhdeniy element
func (cms *CountMinSketch) Count(s string) int {
    min := math.MaxInt32
    for i := 0; i < cms.depth; i++ {
        idx := cms.getHashIndex(s, cms.hashes[i])
        if cms.table[i][idx] < min {
            min = cms.table[i][idx]
        }
    }
    return min
}

func main() {
    width := 10_000
    depth := 5

    cms := NewCountMinSketch(width, depth)
    cms.Init()

    seen := make(map[string]int)
	for i := 0; i < 5_000_000; i++ {
		key := fmt.Sprintf("item-%d", i%1100)
		seen[key]++
		cms.Add(key, 1)
	}

  
    // vyvodim frequency
    for _, word := range []string{"item-1", "item-0", "item-1000", "item--1"} {
        fmt.Printf("Withglasno cms slovo '%s' meetingsaetsya primerno %d raz\t By faktu: %d\n", word, cms.Count(word), seen[word])
    }
}

Pour 5 millions d'insertions, mémoire : 200 Ko. Les opérations sont en O(profondeur) pour l'insertion et la requête.

Comparaison des structures de données

| Caractéristique | HyperLogLog | Count-Min Sketch |

|---------------|-------------|------------------|

Google AdInline article slot

| Tâche | Cardinalité | Fréquence |

| Mémoire | O(log(log N)) | O(d×w) |

| Erreur | 1-3 % | Dépend de d,w |

| Insertion | O(1) | O(profondeur) |

| Requête | O(m) | O(profondeur) |

Points clés :

  • Les deux structures conviennent au traitement en streaming de millions d'éléments
  • HyperLogLog utilise une mémoire fixe indépendamment du volume de données
  • Count-Min Sketch surestime les fréquences en raison des collisions mais fournit des requêtes ponctuelles
  • Pour les systèmes distribués, ajoutez des mutex ou utilisez des opérations atomiques
  • Testez avec des données réelles : l'erreur dépend de la distribution des hachages

— Editorial Team

Advertisement 728x90

Lire ensuite