Benchmark de Concatenação de Strings em Clojure
- #Estrutura de dados
O que é um Benchmark?
Um benchmark é um teste prático que mede o desempenho de um algoritmo, função, sistema ou componente, em termos de tempo, uso de memória, ou outros recursos importantes.
No desenvolvimento de software, benchmarks ajudam a comparar abordagens diferentes para resolver um mesmo problema, identificando qual delas é mais rápida, eficiente ou adequada para determinada situação.
O que é StringBuilder?
StringBuilder
é uma classe do Java que oferece uma forma eficiente de construir strings por meio de um buffer mutável. Diferentemente da concatenação direta de strings, que cria uma nova string a cada operação, o StringBuilder
permite que múltiplas concatenações sejam feitas no mesmo buffer, evitando cópias repetidas e melhorando significativamente a performance.
Objetivo
Este benchmark compara o desempenho de duas abordagens para concatenação de strings em Clojure:
- reduce str (concatenação ingênua)
- StringBuilder (concatenação eficiente)
Motivação
Introdução
Sempre achei interessantes experimentos de performance — especialmente aqueles que colocam lado a lado abordagens diferentes para resolver um mesmo problema. Foi esse tipo de curiosidade que me motivou a testar o impacto real de diferentes formas de concatenar strings em Clojure.
Considerando que Lisp — a família de linguagens da qual Clojure faz parte — tem uma longa história relacionada à gestão eficiente de memória, achei relevante fazer esse benchmark para compartilhar a experiência.
Dessa forma, conseguimos tomar decisões técnicas mais fundamentadas, evitando escolhas baseadas apenas em achismos ou suposições.
Algoritmo (Passo a Passo Funcional)
1. Concatenação ingênua (reduce str)
Pseudocódigo:
- Comece com uma string vazia.
- Para cada elemento da lista de partes:
- Concatene a string acumulada com o próximo elemento usando
str
. - Retorne a string final.
Resumo:
- A cada passo, é criada uma nova string, copiando todo o conteúdo anterior.
- O custo cresce rapidamente conforme o número de partes aumenta.
2. Concatenação eficiente (StringBuilder)
Pseudocódigo:
- Crie um buffer mutável (
StringBuilder
). - Para cada elemento da lista de partes:
- Adicione (append) o elemento ao buffer.
- Ao final, converta o buffer para uma string comum.
Resumo:
- O buffer é reutilizado durante todo o processo.
- O custo cresce linearmente com o número de partes.
Referências de Medição
- Biblioteca: Criterium — ferramenta robusta para benchmarks estatísticos em Clojure.
- Documentação: Criterium no cljdoc, StringBuilder (Java), reduce (Clojure), str (Clojure).
Funções testadas
clojure
CopyEdit
(defn using-str-concat [parts]
(reduce str "" parts))
(defn using-string-builder [parts]
(let [sb (StringBuilder.)]
(doseq [part parts]
(.append sb part))
(.toString sb)))
Metodologia
- Entrada:
clojure
CopyEdit
(def parts (repeat 100000 "abc"))
- 100.000 strings de 3 caracteres cada.
- Execução:
- Cada função é executada usando
criterium.core/quick-benchmark
. - Medição:
- Resultados apresentados em nanossegundos (ns) e em escala logarítmica para facilitar a comparação.
- Reprodutibilidade:
- Rode o código em qualquer ambiente Clojure com as dependências indicadas, em diferentes máquinas, JVMs ou tamanhos de entrada para comparar resultados.
Código-fonte completo
clojure
CopyEdit
(ns clojure-benchmarks.core
(:require [criterium.core :as c]
[incanter.core :as i]
[incanter.charts :as charts]))
(def parts (repeat 100000 "abc"))
(defn using-str-concat [parts]
(reduce str "" parts))
(defn using-string-builder [parts]
(let [sb (StringBuilder.)]
(doseq [part parts]
(.append sb part))
(.toString sb)))
(defn log10 [x]
(Math/log10 (max x 1e-9))) ; evita log(0)
(defn -main [& args]
(println "Benchmark de concatenação de strings em Clojure\n")
(println "reduce str:")
(let [res1 (c/quick-benchmark (using-str-concat parts) {})]
(c/report-result res1)
(println "Valor bruto :mean reduce str:" (:mean res1))
(println "\nStringBuilder:")
(let [res2 (c/quick-benchmark (using-string-builder parts) {})]
(c/report-result res2)
(println "Valor bruto :mean StringBuilder:" (:mean res2))
(let [mean1 (when (and (map? res1) (vector? (:mean res1))) (first (:mean res1)))
mean2 (when (and (map? res2) (vector? (:mean res2))) (first (:mean res2)))]
(when (and (number? mean1) (number? mean2))
(let [labels ["reduce str" "StringBuilder"]
vals [(double mean1) (double mean2)]
logvals (mapv log10 vals)
chart (charts/bar-chart labels logvals
:title "Concatenação de Strings (log₁₀ ns)"
:y-label "log₁₀ Tempo médio (ns)")]
(i/view chart)
(println "\nValores médios (ns):")
(println (format "reduce str : %.2f ns" (first vals)))
(println (format "StringBuilder : %.2f ns" (second vals)))))))))
Resultados Obtidos
reduce str:
- Execution time mean: ~717 ms
- Alto custo por causa da criação de muitas strings intermediárias
StringBuilder:
- Execution time mean: ~855 µs
- Muito mais rápido, custo linear e buffer mutável
Gráfico
Gráfico gerado pelo Incanter mostrando a comparação do tempo médio (em escala logarítmica)
O que o gráfico mostra?
- O título: "Concatenação de Strings (log₁₀ ns)" indica que o gráfico compara o tempo de concatenação de strings.
- O eixo y é o log₁₀ do tempo médio (em nanosegundos) gasto para fazer a concatenação.
- O eixo x mostra dois métodos usados: "reduce str" e "StringBuilder".
- As barras vermelhas representam o valor do tempo médio (no eixo y, em escala logarítmica).
Como interpretar?
- A escala é logarítmica na base 10 (log₁₀), então um valor de -1 equivale a 10⁻¹ = 0,1 ns, um valor de -2 equivale a 10⁻² = 0,01 ns, e assim por diante.
- O método reduce str tem um tempo médio perto de -0.1 (ou seja, aproximadamente 0.8 ns).
- O método StringBuilder tem um tempo médio perto de -3 (ou seja, aproximadamente 0.001 ns).
Como reproduzir e tirar provas reais
- Clone o repositório ou copie o código acima.
- Adicione as dependências no seu
project.clj
:
clojure
CopyEdit
[criterium "0.4.6"]
[incanter "1.9.3"]
- Rode o projeto com
lein run
ou pelo REPL. - Altere o valor de
parts
para testar diferentes tamanhos de entrada. - Compare os resultados em diferentes ambientes.
- Use o gráfico para comunicar visualmente a diferença de desempenho.
Conclusão
Os resultados deste benchmark deixam claro que a escolha do algoritmo de concatenação de strings faz toda a diferença em aplicações que lidam com grandes volumes de dados.
- A abordagem ingênua com
reduce str
é simples, mas ineficiente para listas grandes, pois cria muitas cópias intermediárias e tem custo quadrático. - O uso do
StringBuilder
(ou funções que o utilizam internamente, comoclojure.string/join
) é muito mais eficiente, com custo linear.
Além disso, utilizar ferramentas como Criterium e visualizar os resultados em escala logarítmica são práticas fundamentais para identificar e comunicar diferenças de performance relevantes.
Portanto, ao desenvolver em Clojure (ou em qualquer linguagem JVM), prefira abordagens otimizadas para manipulação de strings quando a performance for um requisito.
Referências
- Criterium – Biblioteca de benchmarking para Clojure
- GitHub: https://github.com/hugoduncan/criterium
- Documentação: https://cljdoc.org/d/criterium/criterium/0.4.6/api/criterium.core
- Incanter – Ambiente de computação estatística e gráficos estilo R para Clojure
- GitHub: https://github.com/incanter/incanter
- Documentação de gráficos: https://incanter.github.io/incanter/charts-api.html
- StringBuilder (Java) – Classe para construção eficiente de strings mutáveis
- Documentação oficial: https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html
- Função
reduce
(Clojure) – Função para redução de coleções
- Documentação: https://clojuredocs.org/clojure.core/reduce
- Função
str
(Clojure) – Função para conversão e concatenação de strings
- Documentação: https://clojuredocs.org/clojure.core/str