A escolha da linguagem de programação adequada para determinadas tarefas pode impactar significativamente o desempenho e a eficiência de uma aplicação. Com o amadurecimento de linguagens modernas, como Julia, que prometem alta performance e facilidade de uso, é natural que comparações com linguagens já consolidadas, como Python, sejam realizadas.

Neste contexto, a análise de desempenho entre linguagens torna-se essencial para entender suas capacidades e limitações em cenários específicos. Para o teste comparativo de velocidade entre as linguagens Python e Julia, foi utilizado a fórmula para calcular o 1º e 2º coeficientes de Assimetria de Pearson, sem o uso de bibliotecas em ambas as linguagens.

Essa abordagem permite avaliar não apenas a eficiência computacional, mas também a clareza e a simplicidade da implementação direta de algoritmos matemáticos em cada uma delas.


O Teste comparativo

Foram gerados dois vetores (listas), um contendo uma quantidade pré-definida de valores entre um intervalo (entre 1 a 1.000.000.000) e o outro contendo as frequências, o número de repetição desses valores, conforme o exemplo abaixo.

Exemplo

Valores Frequências
302
641
974
125
263
Total15

A tabela ao lado mostra um conjunto de dados, composto por 5 elementos contendo os valores e suas frequências, sendo:

  • 2 elementos 30
  • 1 elemento 64
  • 4 elementos 97
  • 5 elementos 12
  • 3 elementos 26
Ao todo, temos 15 elementos, com 5 valores diferentes, demonstrados na matriz abaixo.

$$Dados\ =\ \begin{bmatrix} 30\ \ 30\ \ 64\ \ 97\ \ 97 \\ 97\ \ 97\ \ 12\ \ 12\ \ 12 \\ 12\ \ 12\ \ 26\ \ 26\ \ 26\end{bmatrix}$$


O vetor contendo os valores foi gerado aleatoriamente de acordo com o número de elementos pré-definidos.
As frequências seguiram a lista $[27, 31, 34, 25, 35, 32, 28, 33, 29, 26]$, que contém 10 elementos, e somam o valor total de 300, e, de forma repetitiva, foram duplicados até que se chegou ao número pré-definido, conforme a tabela a seguir:


Testes Número de elementos diferentes Total de elementos gerados
Teste 1501.500
Teste 21003.000
Teste 350015.000
Teste 41.00030.000
Teste 510.000300.000
Teste 650.0001.500.000
Teste 7100.0003.000.000
Teste 8200.0006.000.000
Teste 9500.00015.000.000
Teste 101.000.00030.000.000
Teste 1110.000.000300.000.000
Teste 1250.000.0001.500.000.000
Teste 13100.000.0003.000.000.000

A escolha por gerar dois vetores com valores e frequências separados se dá pelo alto custo computacional que seria gerar um único vetor com todas as repetições.

Cada conjunto de dados foi testado 5 vezes com a fórmula da assimetria, e dos 5 resultados de tempo obtidos, foi retirado uma média. Portanto, cada resultado é a média dos 5 testes.

A escolha por gerar dois vetores com valores e frequências separados se dá pelo alto custo computacional que seria gerar um único vetor com todas as repetições.

Cada conjunto de dados foi testado 5 vezes com a fórmula da assimetria, e dos 5 resultados de tempo obtidos, foi retirado uma média. Portanto, cada resultado do teste comparativo é a média dos 5 testes.



Códigos utilizados

Python
from decimal import Decimal
from pint import UnitRegistry
import math

def mediana(valor, freq):
    # Ordena os valores e suas frequências correspondentes
    valores, frequencias = zip(*sorted(zip(valor, freq)))
    valores = list(valores)
    frequencias = list(frequencias)
    
    # Calcula a soma total das frequências
    total = sum(frequencias)
    
    # Determina o ponto médio
    meio = total / 2
    
    # Calcula as frequências acumuladas
    acumulado = 0
    for i, freq in enumerate(frequencias):
        acumulado += freq
        
        # Caso ímpar: encontra o valor central
        if total % 2 != 0 and acumulado >= meio:
            return valores[i]
        
        # Caso par: encontra os dois valores centrais
        if total % 2 == 0:
            if acumulado > meio:
                return valores[i]
            elif acumulado == meio:
                return (valores[i] + valores[i + 1]) / 2


def assimetria(valor, freq, soma_freq):
    from datetime import datetime
    t1 = datetime.now() # Medindo o tempo inicial

    # Descobrindo o valor da média
    m = sum(map(lambda x, y: x * y, valor, freq)) / soma_freq 
    # Descobrindo o valor da moda
    Moda = valor[freq.index(max(freq))]
    # Descobrindo o valor da mediana
    Med = mediana(valor, freq)
    # Descobrindo o valor do desvio padrão
    s = (sum(map(lambda x, y: (((x-m)**2)*y) / soma_freq, valor, freq))) ** 0.5
    # Calculando os coeficientes de assimetria
    prim = (m - Moda) / s
    seg = 3 * (m - Med) / s

    t2 = datetime.now() # Medindo o tempo final
    diff = t2 - t1  # Diferença de tempo

    return diff.total_seconds()

    
def teste_velocidade(amostra):

    lista = []
    soma_freq = ""
    
    for n in range(5):
        import random
	    
	    # Gerando o vetor com os valores, sem repetição
        valor = random.sample(range(1, 1_000_000_001), amostra)

		# Gerando o vetor contendo as frequências
        sequencia = [27, 31, 34, 25, 35, 32, 28, 33, 29, 26]
        freq = (sequencia * math.ceil(amostra / len(sequencia)))[:amostra]
        
        soma_freq = sum(freq)
      
        
        # Chamando a função e imprimindo os resultados
        tempo_total = assimetria(valor, freq, soma_freq)
    
        lista.append(tempo_total)
    
    media = sum(lista) / len(lista)
   
    num_form = ""
    if str(media).count("e") > 0:
        num_form = f"{Decimal(media):.10f}"
    else:
        num_form = f"{media:.10f}"
    
    if int(num_form.split(".")[0]) > 0:
        resultado = f"Tempo em segundos: {media:.2f} s"
    else:
    
        def contagem_de_zeros(num:float) -> float:
            num_str = f"{Decimal(num):.10f}"
            decimal_part = num_str.split(".")[1]
            
            leading_zeros = []
            for char in decimal_part:
                if char == '0':
                    leading_zeros.append(int(char))
                else:
                    break
            
            return len(leading_zeros)

        
        contagem = contagem_de_zeros(num_form)
        
        # Criar um objeto de registro de unidades
        ureg = UnitRegistry()
        
        # Definir o tempo em segundos
        tempo_em_segundos = float(num_form) * ureg.second
        
        resultado = ""
        
        # Converter para outras unidades
        if contagem in list(range(0,3)):
            tempo_em_milissegundos = tempo_em_segundos.to(ureg.millisecond).magnitude
            resultado = f"Tempo em milissegundos: {tempo_em_milissegundos:.2f} ms"
        
        if contagem in list(range(3,6)):
            tempo_em_microssegundos = tempo_em_segundos.to(ureg.microsecond).magnitude
            resultado = f"Tempo em microssegundos: {tempo_em_microssegundos:.2f} μs"
            
        if contagem in list(range(6,11)):
            tempo_em_nanosegundos = tempo_em_segundos.to(ureg.nanosecond).magnitude
            resultado = f"Tempo em nanosegundos: {tempo_em_nanosegundos:.2f} ns"
    
    return soma_freq, num_form, resultado

for amostra in [50, 100, 500, 1_000, 10_000, 50_000, 100_000, 200_000, 500_000, 1_000_000, 10_000_000, 50_000_000, 100_000_000]:
    tot, num, res = teste_velocidade(amostra)
    print(f"Frequência: {amostra}\nTamanho da amostra: {tot}\ntempo: {num}\n{res}\n\n")

Julia
using StatsBase
using Unitful
using Printf

function mediana(valor, freq)
	# Ordena os valores e as frequências correspondentes
	ordenado = sort(collect(zip(valor, freq)))
	valores = [v[1] for v in ordenado]
	frequencias = [v[2] for v in ordenado]

	# Calcula a soma total das frequências
	total = sum(frequencias)

	# Determina o ponto médio
	meio = total / 2

	# Calcula as frequências acumuladas
	acumulado = 0
	for i in eachindex(frequencias)
		acumulado += frequencias[i]

		# Caso ímpar: encontra o valor central
		if isodd(total) && acumulado >= meio
			return valores[i]
		end

		# Caso par: encontra os dois valores centrais
		if iseven(total)
			if acumulado > meio
				return valores[i]
			elseif acumulado == meio
				return (valores[i] + valores[i + 1]) / 2
			end
		end
	end
end
    

function assimetria(valor, freq, soma_freq)
    # Descobrindo o valor da média
    m = sum(valor .* freq) / soma_freq
    # Descobrindo o valor da moda
    Moda = valor[argmax(freq)]
    # Chamando a função para descobrir o valor da mediana
    Med = mediana(valor, freq)
    # Descobrindo o valor do desvio padrão
    s = sum(((valor .- m).^2) .* freq / soma_freq)^0.5
    # Calculando os coeficientes de assimetria
    prim = (m - Moda) / s
    seg = 3*(m - Med) / s

    return prim, seg
end


function teste_velocidade(amostra)
    
    valores = Vector{Float64}(undef, 6)  # Pré-alocação para 6 elementos
    soma_freq = ""
    
    for n in 1:6
    
	    # Gerando o vetor com os valores, sem repetição
        valor = sort(sample(0:1_000_000_000, amostra; replace=false))
        
        # Gerando o vetor contendo as frequências
        sequencia = [27, 31, 34, 25, 35, 32, 28, 33, 29, 26]
        freq = repeat(sequencia, cld(amostra, length(sequencia)))[1:amostra]
        
        soma_freq = sum(freq)        
      
        # Chamando a função assmetria e calculando o tempo gasto com @elapsed
        tempo_total = @elapsed assimetria(valor, freq, soma_freq)

        # Salvando o tempo gasto em no vetor valores
        valores[n] = tempo_total
    end
    
    media = sum(valores[2:end]) / length(valores[2:end])
    
    
    num_form = ""
    if count(x -> x == 'e', string(media)) > 0
        num_form = @sprintf("%.10f", parse(Float64, string(media)))
    else
        num_form = string(media)
    end

    num_seg = split(num_form, ".")

    if parse(Float64, num_seg[1]) > 0
        resultado = "Tempo em segundos: $(round(parse(Float64, num_form[1:5]), digits=2)) s"
    else

        # Código para retornar o valor formatado, em milissegundos, microssegundos ou nanossegundos        
        # Função para descobrir quantos zeros tem após o ponto
        function contagem_de_zeros(num::Float64)
            num_str = @sprintf("%.10f", parse(Float64, string(num)))
            decimal_part = split(num_str, ".")[2]
        
            # Iterar sobre os caracteres para encontrar zeros iniciais
            leading_zeros = ""
            for char in decimal_part
                if char == '0'
                    leading_zeros *= char
                else
                    break
                end
            end
            
            return length(leading_zeros)
        end
        
        contagem = contagem_de_zeros(parse(Float64, num_form))
        
        # Definir o tempo em segundos
        tempo_em_segundos = parse(Float64, num_form)u"s"
    
        resultado = ""
        
        # Converter para outras unidades
        if contagem in 0:2
            tempo_em_milissegundos = uconvert(u"ms", tempo_em_segundos)
            valor_formatado = @sprintf("%.2f ms", ustrip(tempo_em_milissegundos))
            resultado = "Tempo em milissegundos: $valor_formatado"
        end
        if contagem in 3:5
            tempo_em_microssegundos = uconvert(u"μs", tempo_em_segundos)
            valor_formatado = @sprintf("%.2f μs", ustrip(tempo_em_microssegundos))
            resultado = "Tempo em microssegundos: $valor_formatado"
        end
        if contagem in 6:10
            tempo_em_nanosegundos = uconvert(u"ns", tempo_em_segundos)
            valor_formatado = @sprintf("%.2f ns", ustrip(tempo_em_nanosegundos))
            resultado = "Tempo em nanossegundos: $valor_formatado"
        end
    end

    return soma_freq, num_form, resultado

end

for amostra in [50, 100, 500, 1_000, 10_000, 50_000, 100_000, 200_000, 500_000, 1_000_000, 10_000_000, 50_000_000, 100_000_000]
    tot, num, res = teste_velocidade(amostra)
    println("Frequência: $amostra\nTamanho da amostra: $tot\ntempo: $num\n$res\n\n")
end

Os dois códigos foram criados para serem os mais semelhantes possível, tendo utilizado o mínimo de bibliotecas externas para rodar e apresentar os resultados, e ambos foram pré-otimizados com as bibliotecas Profile e Cprofile.

Os códigos foram testados em um computador contendo um processador Ryzen 5700G, 32Gb de Ram e armazenamento NVMe, sem uma placa de vídeo dedicada, e com sistema operacional Linux.

Os resultados foram obtidos em segundos (seg), e convertidos, para melhor compreensão, em unidades de medidas temporais anteriores e posteriores, como descritas a seguir.

Abaixo, o exemplo de 1 segundo convertido em unidades de medida maiores e menores:

  • Microssegundos (μs).. 1.000.000 μs
  • Milissegundos (ms)….. 1.000 ms
  • Segundos (seg)…………..1.0 seg
  • Minutos (min)…………… 0,0166666667 min

Resultados

Elementos Resultados
Teste Número de elementos Elementos gerados Python Julia
1 50 1.500 28.80 μs 1.98 μs
2 100 3.000 45.80 μs 2.86 μs
3 500 15.000 242.40 μs 7.62 μs
4 1.000 30.000 519.40 μs 9.88 μs
5 10.000 300.000 5.78 ms 273.08 μs
6 50.000 1.500.000 32.81 ms 1.92 ms
7 100.000 3.000.000 93.22 ms 3.21 ms
8 200.000 6.000.000 272.47 ms 13.56 ms
9 500.000 15.000.000 845.75 ms 17.52 ms
10 1.000.000 30.000.000 1.88 seg 39.12 ms
11 10.000.000 300.000.000 23.60 seg 223.17 ms
12 50.000.000 1.500.000.000 2:40 min 778.71 ms
13 100.000.000 3.000.000.000 5:15 min 1.47 seg

Os resultados do teste comparativo foram bem interessantes. No último experimento mesmo, enquanto Python demorou 5 minutos e 15 segundos para executar o teste, Julia executou em 1.47 segundos. É uma diferença impressionante!

O desempenho de Julia foi superior ao de Python em todos os casos analisados, embora a diferença não tenha sido exponencial. A menor variação foi observada no primeiro resultado, em que Julia foi 14.51 vezes mais rápida que Python. Já a maior variação ocorreu no último resultado, com Julia sendo 209.90 vezes mais rápida que Python. 

O gráfico interativo abaixo ilustra o resultado de cada teste.

Movimente o cursor para exibir as diferenças entre os resultados

Não foram feitos mais testes com valores amostrais maiores, como 200, 500 milhões ou 1 bilhão de valores únicos, por limitações no uso da memória do computador utilizado.