Eu estava implementando um algoritmo no Swift Beta e percebi que o desempenho era muito ruim. Depois de cavar mais fundo, percebi que um dos gargalos era algo tão simples quanto classificar arrays. A parte relevante está aqui:
let n = 1000000
var x = [Int](repeating: 0, count: n)
for i in 0..<n {
x[i] = random()
}
// start clock here
let y = sort(x)
// stop clock here
Em C ++, uma operação semelhante leva 0,06s no meu computador.
No Python, são necessários 0,6s (sem truques, apenas y = classificado (x) para uma lista de números inteiros).
No Swift, são necessários 6s se eu o compilar com o seguinte comando:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
E são necessários 88s se eu compilar com o seguinte comando:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Os tempos no Xcode com compilações "Release" vs. "Debug" são semelhantes.
O que há de errado aqui? Eu pude entender alguma perda de desempenho em comparação com C ++, mas não uma desaceleração de 10 vezes em comparação com Python puro.
Edit: weather percebeu que mudar -O3
para -Ofast
faz com que esse código seja executado quase tão rápido quanto a versão C ++! No entanto, -Ofast
altera muito a semântica do idioma - nos meus testes, desabilitou as verificações de excesso de números inteiros e de indexação de array . Por exemplo, com -Ofast
o seguinte código Swift é executado silenciosamente sem travar (e imprime algum lixo):
let n = 10000000
print(n*n*n*n*n)
let x = [Int](repeating: 10, count: n)
print(x[n])
assim -Ofast
não é o que queremos; o ponto principal da Swift é que temos as redes de segurança no lugar. Obviamente, as redes de segurança têm algum impacto no desempenho, mas não devem tornar os programas 100 vezes mais lentos. Lembre-se de que o Java já verifica os limites da matriz e, em casos típicos, a desaceleração é por um fator muito menor que 2. E no Clang e no GCC, conseguimos -ftrapv
verificar a sobrecarga de números inteiros (assinados), e também não é tão lento.
Daí a pergunta: como podemos obter um desempenho razoável no Swift sem perder as redes de segurança?
Edit 2: Fiz mais alguns testes comparativos, com loops muito simples ao longo das linhas de
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(Aqui a operação xor existe apenas para que eu possa encontrar mais facilmente o loop relevante no código de montagem. Tentei escolher uma operação fácil de detectar, mas também "inofensiva", no sentido de que não deveria exigir nenhuma verificação relacionada para um número inteiro excede.)
Novamente, havia uma enorme diferença no desempenho entre -O3
e -Ofast
. Então, dei uma olhada no código do assembly:
Com
-Ofast
eu recebo praticamente o que eu esperaria. A parte relevante é um loop com 5 instruções em linguagem de máquina.Com
-O3
eu recebo algo que estava além da minha imaginação mais louca. O loop interno abrange 88 linhas de código de montagem. Não tentei entender tudo, mas as partes mais suspeitas são 13 invocações de "callq _swift_retain" e outras 13 invocações de "callq _swift_release". Ou seja, 26 chamadas de sub-rotina no loop interno !
Edit 3: Nos comentários, Ferruccio solicitou benchmarks justos no sentido de que eles não dependem de funções internas (por exemplo, classificação). Eu acho que o seguinte programa é um bom exemplo:
let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
for j in 0..<n {
x[i] = x[j]
}
}
Não há aritmética, portanto, não precisamos nos preocupar com estouros de número inteiro. A única coisa que fazemos é apenas muitas referências de matriz. E os resultados estão aqui - Swift -O3 perde quase um fator de 500 em comparação com -Ofast:
- C ++ -O3: 0,05 s
- C ++ -0: 0,4 s
- Java: 0,2 s
- Python com PyPy: 0,5 s
- Python: 12 s
- Swift -Ofast: 0,05 s
- Swift -O3: 23 s
- Swift -O0: 443 s
(Se você estiver preocupado com o fato de o compilador otimizar totalmente os loops inúteis, você pode alterá-lo para x[i] ^= x[j]
, por exemplo , e adicionar uma declaração de impressão que seja impressa x[0]
. Isso não muda nada; os tempos serão muito semelhantes.)
E sim, aqui a implementação do Python era uma implementação pura e estúpida do Python, com uma lista de entradas e aninhadas para loops. Deve ser muito mais lento que o Swift não otimizado. Algo parece estar seriamente quebrado com a Swift e a indexação de array.
Edição 4: esses problemas (assim como outros problemas de desempenho) parecem ter sido corrigidos no Xcode 6 beta 5.
Para a classificação, agora tenho os seguintes horários:
- clang ++ -O3: 0,06 s
- swiftc -Ofast: 0,1 s
- swiftc -O: 0,1 s
- swiftc: 4 s
Para loops aninhados:
- clang ++ -O3: 0,06 s
- swiftc -Ofast: 0,3 s
- swiftc -O: 0,4 s
- swiftc: 540 s
Parece que não há mais motivo para usar o inseguro -Ofast
(aka -Ounchecked
); plain -O
produz um código igualmente bom.
fonte
xcrun --sdk macosx swift -O3
. É mais curto.Respostas:
tl; dr Swift 1.0 agora é tão rápido quanto C por essa referência usando o nível de otimização de versão padrão [-O].
Aqui está um quicksort no local no Swift Beta:
E o mesmo em C:
Ambos funcionam:
Ambos são chamados no mesmo programa que foi escrito.
Isso converte os tempos absolutos em segundos:
Aqui está um resumo dos níveis de otimização do compilador:
Tempo em segundos com [-Onone] para n = 10_000 :
Aqui está o sortin interno do Swift para n = 10_000 :
Aqui está [-O] para n = 10_000 :
Como você pode ver, o desempenho do Swift melhorou em um fator de 20.
Conforme a resposta de mweathers , a configuração [-Ofast] faz a diferença real, resultando nestes tempos para n = 10_000 :
E para n = 1_000_000 :
Para comparação, isso é com [-Onone] para n = 1_000_000 :
Portanto, o Swift sem otimizações foi quase 1000x mais lento que C neste benchmark, nesta fase de seu desenvolvimento. Por outro lado, com os dois compiladores configurados para [-Ofast], o Swift realmente teve um desempenho tão bom quanto não um pouco melhor que C.
Foi apontado que [-Ofast] altera a semântica da linguagem, tornando-a potencialmente insegura. É o que a Apple afirma nas notas de versão do Xcode 5.0:
Todos eles defendem isso. Seja isso sensato ou não, não sei dizer, mas pelo que posso dizer, parece razoável usar [-Ofast] em um release se você não estiver fazendo aritmética de ponto flutuante de alta precisão e não tem certeza de que é um número inteiro ou possíveis estouros de matriz no seu programa. Se você precisar de verificações de alto desempenho e estouro / aritmética precisa, escolha outro idioma por enquanto.
ATUALIZAÇÃO BETA 3:
n = 10_000 com [-O] :
O Swift em geral é um pouco mais rápido e parece que o tipo interno do Swift mudou bastante.
ATUALIZAÇÃO FINAL:
[-Onone] :
[-O] :
[-Ounchecked] :
fonte
TL; DR : Sim, a única implementação da linguagem Swift é lenta no momento . Se você precisar de um código rápido e numérico (e outros tipos de código, presumivelmente), basta usar outro. No futuro, você deve reavaliar sua escolha. Porém, pode ser bom o suficiente para a maioria dos códigos de aplicativos gravados em um nível superior.
Pelo que estou vendo no SIL e no LLVM IR, parece que eles precisam de várias otimizações para remover retenções e lançamentos, que podem ser implementadas no Clang (para Objective-C), mas ainda não as portaram. Essa é a teoria que estou adotando (por enquanto ... ainda preciso confirmar que Clang faz algo a respeito), uma vez que um criador de perfil executado no último caso de teste dessa pergunta produz esse resultado "bonito":
Como foi dito por muitos outros,
-Ofast
é totalmente inseguro e altera a semântica da linguagem. Para mim, está no estágio "Se você vai usar isso, use outro idioma". Vou reavaliar essa escolha mais tarde, se ela mudar.-O3
nos chamaswift_retain
eswift_release
chama que, honestamente, não parece que eles deveriam estar lá para este exemplo. O otimizador deveria ter elegido (a maioria) AFAICT, pois conhece a maioria das informações sobre a matriz e sabe que possui (pelo menos) uma forte referência a ela.Ele não deve emitir mais retenções quando não está chamando funções que possam liberar os objetos. Eu não acho que um construtor de matriz possa retornar uma matriz menor do que o solicitado, o que significa que muitas verificações emitidas são inúteis. Ele também sabe que o número inteiro nunca estará acima de 10k, portanto, as verificações de estouro podem ser otimizadas (não por
-Ofast
estranheza, mas por causa da semântica do idioma (nada mais está mudando esse var nem pode acessá-lo e somando 10k é seguro para o tipoInt
).O compilador pode não ser capaz de desmarcar a matriz ou os elementos da matriz, já que eles são passados para
sort()
, o que é uma função externa e precisa receber os argumentos que espera. Isso nos fará ter que usar osInt
valores indiretamente, o que tornaria um pouco mais lento. Isso poderia mudar se asort()
função genérica (não da maneira multi-método) estivesse disponível para o compilador e fosse incorporada.Este é um idioma muito novo (publicamente), e está passando por muitas mudanças, já que há pessoas (pesadamente) envolvidas com o idioma Swift pedindo feedback e todos dizem que o idioma não está terminado e será mudança.
Código usado:
PS: Não sou especialista em Objective-C nem em todas as instalações do Cocoa , Objective-C ou Swift. Eu também poderia estar assumindo algumas coisas que não escrevi.
fonte
-Ofast
"totalmente inseguro"? Supondo que você saiba como testar seu código e descartar estouros.-Ofast
também altera a semântica do idioma, mas não posso financiar nenhum documento para isso. Como você pode ter certeza de que sabe o que está fazendo?Int[]
', pois isso depende do nível que o compilador pode alinhar e deve poder alinhar os loops apertados com / após algumas orientações.Decidi dar uma olhada nisso por diversão, e aqui estão os horários que recebo:
Rápido
Resultados:
Swift 1.1
Swift 1.2
Swift 2.0
Parece ser o mesmo desempenho se eu compilar com
-Ounchecked
.Swift 3.0
Parece ter havido uma regressão de desempenho do Swift 2.0 para o Swift 3.0, e também estou vendo uma diferença entre
-O
e-Ounchecked
pela primeira vez.Swift 4.0
O Swift 4 melhora o desempenho novamente, mantendo um espaço entre
-O
e-Ounchecked
.-O -whole-module-optimization
não pareceu fazer a diferença.C ++
Resultados:
Apple Clang 6.0
Apple Clang 6.1.0
Apple Clang 7.0.0
Apple Clang 8.0.0
Apple Clang 9.0.0
Veredito
No momento da redação deste artigo, a classificação do Swift é rápida, mas ainda não tão rápida quanto a classificação do C ++, quando compilada
-O
com os compiladores e bibliotecas acima. Com-Ounchecked
, parece ser tão rápido quanto o C ++ no Swift 4.0.2 e no Apple LLVM 9.0.0.fonte
De
The Swift Programming Language
:A
sort
função tem duas declarações.A declaração padrão que permite especificar um fechamento de comparação:
E uma segunda declaração que usa apenas um único parâmetro (a matriz) e é "codificada para usar o comparador menor que".
Testei uma versão modificada do seu código em um playground, com o fechamento adicionado para que eu pudesse monitorar a função um pouco mais de perto, e descobri que com n definido como 1000, o fechamento era chamado cerca de 11.000 vezes.
Não é uma função eficiente, eu recomendaria o uso de uma melhor implementação da função de classificação.
EDITAR:
Dei uma olhada na página da Wikipedia do Quicksort e escrevi uma implementação Swift para ela. Aqui está o programa completo que eu usei (em um playground)
Usando isso com n = 1000, descobri que
Parece que o método de classificação interno é (ou está perto de) uma classificação rápida e muito lento ...
fonte
2*n*log(n)
. Essas são 13815 comparações para classificar n = 1000 elementos, portanto, se a função de comparação for chamada cerca de 11000 vezes, isso não parecerá tão ruim.log(n)
para complexidade algorítmica convencionalmente refere-se ao log base-2. O motivo para não declarar a base é que a lei de mudança de base para logaritmos introduz apenas um multiplicador constante, que é descartado para os fins da notação O.C(n) = 2n ln n ≈ 1.39n log₂ n
. Para n = 1,000 isto dá C (n) = 13815, e que é não um "notação big-O".A partir do Xcode 7, você pode ativar
Fast, Whole Module Optimization
. Isso deve aumentar seu desempenho imediatamente.fonte
Desempenho do Swift Array revisitado:
Eu escrevi meu próprio benchmark comparando Swift com C / Objective-C. Meu benchmark calcula números primos. Ele usa a matriz de números primos anteriores para procurar fatores primos em cada novo candidato, por isso é bastante rápido. No entanto, ele faz toneladas de leitura de matriz e menos grava em matrizes.
Eu originalmente fiz esse benchmark contra o Swift 1.2. Decidi atualizar o projeto e executá-lo no Swift 2.0.
O projeto permite selecionar entre o uso de matrizes swift normais e o uso de buffers de memória não seguros Swift usando a semântica de matrizes.
Para C / Objective-C, você pode optar por usar NSArrays ou C malloc'ed matrizes.
Os resultados do teste parecem ser bastante semelhantes com a otimização mais rápida e menor do código ([-0s]) ou a otimização mais rápida e agressiva ([-0fast]).
O desempenho do Swift 2.0 ainda é horrível com a otimização do código desativada, enquanto o desempenho do C / Objective-C é apenas moderadamente mais lento.
A linha inferior é que os cálculos baseados em array de C malloc são os mais rápidos, por uma margem modesta
O Swift com buffers inseguros leva cerca de 1,19X a 1,20X mais do que as matrizes C malloc'd ao usar a menor e mais rápida otimização de código. a diferença parece um pouco menor com a otimização rápida e agressiva (o Swift demora mais entre 1,18x e 1,16x mais que C.
Se você usa matrizes Swift regulares, a diferença com C é um pouco maior. (Swift leva ~ 1,22 a 1,23 mais.)
Matrizes Swift regulares são
DRAMATICALLY
mais rápidas do que no Swift 1.2 / Xcode 6. Seu desempenho é tão próximo das matrizes baseadas em buffer inseguro do Swift que o uso de buffers de memória não seguros não parece mais valer a pena, que é grande.BTW, Objective-C NSArray desempenho fede. Se você usar os objetos de contêiner nativos nos dois idiomas, o Swift é DRAMATICAMENTE mais rápido.
Você pode conferir meu projeto no github em SwiftPerformanceBenchmark
Ele possui uma interface simples que facilita a coleta de estatísticas.
É interessante que a classificação pareça ser um pouco mais rápida no Swift do que em C agora, mas esse algoritmo de número principal ainda é mais rápido no Swift.
fonte
A principal questão mencionada por outras pessoas, mas que não é mencionada o suficiente, é que
-O3
nada faz no Swift (e nunca o fez); portanto, quando compilado, ele é efetivamente não otimizado (-Onone
).Os nomes das opções foram alterados ao longo do tempo, portanto, outras respostas têm sinalizadores obsoletos para as opções de construção. As opções atuais corretas (Swift 2.2) são:
A otimização de todo o módulo tem uma compilação mais lenta, mas pode otimizar os arquivos dentro do módulo, ou seja, dentro de cada estrutura e dentro do código real do aplicativo, mas não entre eles. Você deve usar isso para qualquer coisa crítica ao desempenho)
Você também pode desativar as verificações de segurança para obter ainda mais velocidade, mas com todas as asserções e condições prévias, não apenas desativadas, mas otimizadas com base em que elas estão corretas. Se você alguma vez acertar uma afirmação, isso significa que você está em um comportamento indefinido. Use com extrema cautela e somente se determinar que o aumento de velocidade vale a pena para você (por meio de testes). Se você achar que é valioso para algum código, recomendo separá-lo em uma estrutura separada e desabilitar apenas as verificações de segurança desse módulo.
fonte
Este é o meu blog sobre Quick Sort - Github sample Quick-Sort
Você pode dar uma olhada no algoritmo de particionamento do Lomuto em Particionando a lista. Escrito em Swift.
fonte
O Swift 4.1 introduz um novo
-Osize
modo de otimização.Leitura adicional: https://swift.org/blog/osize/
fonte