Desempenho do Swift Beta: classificando matrizes


929

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 -O3para -Ofastfaz com que esse código seja executado quase tão rápido quanto a versão C ++! No entanto, -Ofastaltera 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 -Ofasto 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 -ftrapvverificar 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 -O3e -Ofast. Então, dei uma olhada no código do assembly:

  • Com -Ofasteu recebo praticamente o que eu esperaria. A parte relevante é um loop com 5 instruções em linguagem de máquina.

  • Com -O3eu 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 -Oproduz um código igualmente bom.


20
Aqui está outro "Swift 100 vezes mais lento do que C" pergunta: stackoverflow.com/questions/24102609/...
Jukka Suomela

16
E aqui é a discussão sobre material de marketing da Apple relacionadas com o desempenho de Swift boa na classificação: programmers.stackexchange.com/q/242816/913
Jukka Suomela

2
Você pode compilar com: xcrun --sdk macosx swift -O3. É mais curto.
Southern Hospitality

3
Este link mostra algumas outras operações básicas em comparação com o Objective-C.
Wold

4
Com o Beta 5, houve uma melhora substancial na velocidade de Swift - veja este post de Jesse Squires para obter mais detalhes.
Nate Cook

Respostas:


460

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:

func quicksort_swift(inout a:CInt[], start:Int, end:Int) {
    if (end - start < 2){
        return
    }
    var p = a[start + (end - start)/2]
    var l = start
    var r = end - 1
    while (l <= r){
        if (a[l] < p){
            l += 1
            continue
        }
        if (a[r] > p){
            r -= 1
            continue
        }
        var t = a[l]
        a[l] = a[r]
        a[r] = t
        l += 1
        r -= 1
    }
    quicksort_swift(&a, start, r + 1)
    quicksort_swift(&a, r + 1, end)
}

E o mesmo em C:

void quicksort_c(int *a, int n) {
    if (n < 2)
        return;
    int p = a[n / 2];
    int *l = a;
    int *r = a + n - 1;
    while (l <= r) {
        if (*l < p) {
            l++;
            continue;
        }
        if (*r > p) {
            r--;
            continue;
        }
        int t = *l;
        *l++ = *r;
        *r-- = t;
    }
    quicksort_c(a, r - a + 1);
    quicksort_c(l, a + n - l);
}

Ambos funcionam:

var a_swift:CInt[] = [0,5,2,8,1234,-1,2]
var a_c:CInt[] = [0,5,2,8,1234,-1,2]

quicksort_swift(&a_swift, 0, a_swift.count)
quicksort_c(&a_c, CInt(a_c.count))

// [-1, 0, 2, 2, 5, 8, 1234]
// [-1, 0, 2, 2, 5, 8, 1234]

Ambos são chamados no mesmo programa que foi escrito.

var x_swift = CInt[](count: n, repeatedValue: 0)
var x_c = CInt[](count: n, repeatedValue: 0)
for var i = 0; i < n; ++i {
    x_swift[i] = CInt(random())
    x_c[i] = CInt(random())
}

let swift_start:UInt64 = mach_absolute_time();
quicksort_swift(&x_swift, 0, x_swift.count)
let swift_stop:UInt64 = mach_absolute_time();

let c_start:UInt64 = mach_absolute_time();
quicksort_c(&x_c, CInt(x_c.count))
let c_stop:UInt64 = mach_absolute_time();

Isso converte os tempos absolutos em segundos:

static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MSEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MSEC;

mach_timebase_info_data_t timebase_info;

uint64_t abs_to_nanos(uint64_t abs) {
    if ( timebase_info.denom == 0 ) {
        (void)mach_timebase_info(&timebase_info);
    }
    return abs * timebase_info.numer  / timebase_info.denom;
}

double abs_to_seconds(uint64_t abs) {
    return abs_to_nanos(abs) / (double)NANOS_PER_SEC;
}

Aqui está um resumo dos níveis de otimização do compilador:

[-Onone] no optimizations, the default for debug.
[-O]     perform optimizations, the default for release.
[-Ofast] perform optimizations and disable runtime overflow checks and runtime type checks.

Tempo em segundos com [-Onone] para n = 10_000 :

Swift:            0.895296452
C:                0.001223848

Aqui está o sortin interno do Swift para n = 10_000 :

Swift_builtin:    0.77865783

Aqui está [-O] para n = 10_000 :

Swift:            0.045478346
C:                0.000784666
Swift_builtin:    0.032513488

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 :

Swift:            0.000706745
C:                0.000742374
Swift_builtin:    0.000603576

E para n = 1_000_000 :

Swift:            0.107111846
C:                0.114957179
Swift_sort:       0.092688548

Para comparação, isso é com [-Onone] para n = 1_000_000 :

Swift:            142.659763258
C:                0.162065333
Swift_sort:       114.095478272

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:

Um novo nível de otimização -Ofast, disponível no LLVM, permite otimizações agressivas. -Ofast relaxa algumas restrições conservadoras, principalmente para operações de ponto flutuante, que são seguras para a maioria dos códigos. Pode gerar ganhos significativos de alto desempenho do compilador.

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] :

Swift:            0.019697268
C:                0.000718064
Swift_sort:       0.002094721

O Swift em geral é um pouco mais rápido e parece que o tipo interno do Swift mudou bastante.

ATUALIZAÇÃO FINAL:

[-Onone] :

Swift:   0.678056695
C:       0.000973914

[-O] :

Swift:   0.001158492
C:       0.001192406

[-Ounchecked] :

Swift:   0.000827764
C:       0.001078914

25
O uso de -emit-sil para gerar o código SIL intermediário mostra o que está sendo retido (argh, o estouro de pilha está impossibilitando a formatação). É um objeto de buffer interno na matriz. Definitivamente, isso soa como um bug do otimizador, o otimizador do ARC deve ser capaz de remover as retenções sem -Ofast.
precisa saber é o seguinte

Apenas discordo de que precisamos usar outro idioma se quiser usar as otimizações do Ofast. Ele terá que lidar da mesma forma com a questão de checagem de limites e outros problemas menores, se escolher outro idioma como C. O swift é legal exatamente porque deve ser seguro por padrão e, opcionalmente, rápido e inseguro, se necessário. Isso permite que o programador depure seu código também, para garantir que tudo esteja correto e compilar usando o Ofast. A possibilidade de usar padrões modernos e ainda ter o poder de uma linguagem "insegura" como C é muito legal.
Wallacy

2
se você pode me dizer como isso pode ser inválido, por favor. Eu sempre gosto de saber mais
Joseph Mark

3
fez uma atualização final, o Swift agora é tão rápido quanto C por essa referência usando otimizações padrão.
Joseph Mark

4
Dica: As implementações Swift e C do quicksort podem ser melhoradas se você recursar primeiro na menor partição! (Em vez de recursar sempre na partição esquerda.) O Quicksort implementado com uma simples seleção dinâmica no pior dos casos leva tempo O (n ^ 2), mas mesmo nesse pior caso, você só precisa de espaço de pilha O (log n) recorrendo na partição menor primeiro.
Macneil Shonle 16/03/2015

108

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":

Criação de perfil de tempo em -O3 Criação de perfil de tempo em -Ofast

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.

-O3nos chama swift_retaine swift_releasechama 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 -Ofastestranheza, mas por causa da semântica do idioma (nada mais está mudando esse var nem pode acessá-lo e somando 10k é seguro para o tipo Int).

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 os Intvalores indiretamente, o que tornaria um pouco mais lento. Isso poderia mudar se a sort()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:

import Cocoa

let swift_start = NSDate.timeIntervalSinceReferenceDate();
let n: Int = 10000
let x = Int[](count: n, repeatedValue: 1)
for i in 0..n {
    for j in 0..n {
        let tmp: Int = x[j]
        x[i] = tmp
    }
}
let y: Int[] = sort(x)
let swift_stop = NSDate.timeIntervalSinceReferenceDate();

println("\(swift_stop - swift_start)s")

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.


O compilador pode não ser capaz de desmarcar a matriz ou os elementos da matriz, já que eles são passados ​​para sort (), que é uma função externa e precisa receber os argumentos que espera. Isso não deve importar para um compilador relativamente bom. Passar metadados (no ponteiro - 64bits oferecem muito dique) sobre os dados reais e ramificá-los na função chamada.
bestsss

3
O que exatamente torna -Ofast"totalmente inseguro"? Supondo que você saiba como testar seu código e descartar estouros.
Joseph Mark

@sjeohp: Isso na verdade está assumindo muito :-) É difícil fazer uma verificação do código e excluir estouros. Pela minha experiência (trabalho no compilador e verifiquei algumas grandes bases de código), e o que ouvi de pessoas que o compilam trabalha em grandes empresas, é difícil obter estouros e outros comportamentos indefinidos . Até o conselho da Apple (apenas um exemplo) sobre a correção do UB está errado, às vezes ( randomascii.wordpress.com/2014/04/17/… ). -Ofasttambé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?
filcab

@ bestsss: é possível, mas pode não ser útil. Ele adiciona verificações em todos os acessos a um Int []. Depende se matrizes de Int e alguns outros tipos primitivos (você tem, no máximo, 3 bits) são muito utilizados (especialmente quando você pode diminuir para C, se necessário). Ele também usa alguns bits que eles podem querer usar se, eventualmente, quiserem adicionar um GC não-ARC. Também não é escalável para genéricos com mais de um argumento. Como eles têm todos os tipos, seria muito mais fácil especializar todo o código que tocou em Int [] (mas não Int? []) Para usar o Int. Mas então você precisa da interoperabilidade de Obj-C.
filcab

O @filcab, GC não-ARC (ou seja, real) seria realmente útil, mas eles precisam de algo que não seja compatível com C se quiserem um GC verdadeiramente concorrente e não-STW. Eu não me preocuparia com 'todo acesso a 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.
bestsss 10/06

53

Decidi dar uma olhada nisso por diversão, e aqui estão os horários que recebo:

Swift 4.0.2           :   0.83s (0.74s with `-Ounchecked`)
C++ (Apple LLVM 8.0.0):   0.74s

Rápido

// Swift 4.0 code
import Foundation

func doTest() -> Void {
    let arraySize = 10000000
    var randomNumbers = [UInt32]()

    for _ in 0..<arraySize {
        randomNumbers.append(arc4random_uniform(UInt32(arraySize)))
    }

    let start = Date()
    randomNumbers.sort()
    let end = Date()

    print(randomNumbers[0])
    print("Elapsed time: \(end.timeIntervalSince(start))")
}

doTest()

Resultados:

Swift 1.1

xcrun swiftc --version
Swift version 1.1 (swift-600.0.54.20)
Target: x86_64-apple-darwin14.0.0

xcrun swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 1.02204304933548

Swift 1.2

xcrun swiftc --version
Apple Swift version 1.2 (swiftlang-602.0.49.6 clang-602.0.49)
Target: x86_64-apple-darwin14.3.0

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.738763988018036

Swift 2.0

xcrun swiftc --version
Apple Swift version 2.0 (swiftlang-700.0.59 clang-700.0.72)
Target: x86_64-apple-darwin15.0.0

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.767306983470917

Parece ser o mesmo desempenho se eu compilar com -Ounchecked.

Swift 3.0

xcrun swiftc --version
Apple Swift version 3.0 (swiftlang-800.0.46.2 clang-800.0.38)
Target: x86_64-apple-macosx10.9

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.939633965492249

xcrun -sdk macosx swiftc -Ounchecked SwiftSort.swift
./SwiftSort     
Elapsed time: 0.866258025169373

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 -Oe -Ouncheckedpela primeira vez.

Swift 4.0

xcrun swiftc --version
Apple Swift version 4.0.2 (swiftlang-900.0.69.2 clang-900.0.38)
Target: x86_64-apple-macosx10.9

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.834299981594086

xcrun -sdk macosx swiftc -Ounchecked SwiftSort.swift
./SwiftSort     
Elapsed time: 0.742045998573303

O Swift 4 melhora o desempenho novamente, mantendo um espaço entre -Oe -Ounchecked. -O -whole-module-optimizationnão pareceu fazer a diferença.

C ++

#include <chrono>
#include <iostream>
#include <vector>
#include <cstdint>
#include <stdlib.h>

using namespace std;
using namespace std::chrono;

int main(int argc, const char * argv[]) {
    const auto arraySize = 10000000;
    vector<uint32_t> randomNumbers;

    for (int i = 0; i < arraySize; ++i) {
        randomNumbers.emplace_back(arc4random_uniform(arraySize));
    }

    const auto start = high_resolution_clock::now();
    sort(begin(randomNumbers), end(randomNumbers));
    const auto end = high_resolution_clock::now();

    cout << randomNumbers[0] << "\n";
    cout << "Elapsed time: " << duration_cast<duration<double>>(end - start).count() << "\n";

    return 0;
}

Resultados:

Apple Clang 6.0

clang++ --version
Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.0.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.688969

Apple Clang 6.1.0

clang++ --version
Apple LLVM version 6.1.0 (clang-602.0.49) (based on LLVM 3.6.0svn)
Target: x86_64-apple-darwin14.3.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.670652

Apple Clang 7.0.0

clang++ --version
Apple LLVM version 7.0.0 (clang-700.0.72)
Target: x86_64-apple-darwin15.0.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.690152

Apple Clang 8.0.0

clang++ --version
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.68253

Apple Clang 9.0.0

clang++ --version
Apple LLVM version 9.0.0 (clang-900.0.38)
Target: x86_64-apple-darwin16.7.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.736784

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 -Ocom 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.


2
Na realidade, você nunca deve chamar vector :: reserve () antes de inserir dez milhões de elementos.
BJovke

Possivelmente! Somente o tipo está sendo cronometrado no momento.
Aprenda OpenGL ES

34

De The Swift Programming Language:

A biblioteca padrão da Função de classificação Swift fornece uma função chamada classificação, que classifica uma matriz de valores de um tipo conhecido, com base na saída de um fechamento de classificação que você fornece. Depois de concluir o processo de classificação, a função de classificação retorna uma nova matriz do mesmo tipo e tamanho da antiga, com seus elementos na ordem correta.

A sortfunção tem duas declarações.

A declaração padrão que permite especificar um fechamento de comparação:

func sort<T>(array: T[], pred: (T, T) -> Bool) -> T[]

E uma segunda declaração que usa apenas um único parâmetro (a matriz) e é "codificada para usar o comparador menor que".

func sort<T : Comparable>(array: T[]) -> T[]

Example:
sort( _arrayToSort_ ) { $0 > $1 }

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.

let n = 1000
let x = Int[](count: n, repeatedValue: 0)
for i in 0..n {
    x[i] = random()
}
let y = sort(x) { $0 > $1 }

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)

import Foundation

func quickSort(inout array: Int[], begin: Int, end: Int) {
    if (begin < end) {
        let p = partition(&array, begin, end)
        quickSort(&array, begin, p - 1)
        quickSort(&array, p + 1, end)
    }
}

func partition(inout array: Int[], left: Int, right: Int) -> Int {
    let numElements = right - left + 1
    let pivotIndex = left + numElements / 2
    let pivotValue = array[pivotIndex]
    swap(&array[pivotIndex], &array[right])
    var storeIndex = left
    for i in left..right {
        let a = 1 // <- Used to see how many comparisons are made
        if array[i] <= pivotValue {
            swap(&array[i], &array[storeIndex])
            storeIndex++
        }
    }
    swap(&array[storeIndex], &array[right]) // Move pivot to its final place
    return storeIndex
}

let n = 1000
var x = Int[](count: n, repeatedValue: 0)
for i in 0..n {
    x[i] = Int(arc4random())
}

quickSort(&x, 0, x.count - 1) // <- Does the sorting

for i in 0..n {
    x[i] // <- Used by the playground to display the results
}

Usando isso com n = 1000, descobri que

  1. quickSort () foi chamado cerca de 650 vezes,
  2. cerca de 6000 swaps foram feitos,
  3. e existem cerca de 10.000 comparações

Parece que o método de classificação interno é (ou está perto de) uma classificação rápida e muito lento ...


17
Talvez eu esteja completamente errado, mas de acordo com en.wikipedia.org/wiki/Quicksort , o número médio de comparações no Quicksort é 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.
Martin R

6
A Apple também afirmou que uma "classificação complexa de objetos" (seja o que for) é 3,9 vezes mais rápida no Swift do que no Python. Portanto, não deve ser necessário encontrar uma "melhor função de classificação". - Mas Swift ainda está em desenvolvimento ...
Martin R

6
Ele não se referem ao logaritmo natural.
Martin R

24
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.
minuteman3

3
Com relação à discussão sobre logaritmo natural versus logaritmo de base 2: A declaração precisa da página da Wikipedia é que o número médio de comparações necessárias para n elementos é 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".
Martin R

18

A partir do Xcode 7, você pode ativar Fast, Whole Module Optimization. Isso deve aumentar seu desempenho imediatamente.

insira a descrição da imagem aqui


12

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.


8

A principal questão mencionada por outras pessoas, mas que não é mencionada o suficiente, é que -O3nada 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:

-Onone // Debug - slow
-O     // Optimised
-O -whole-module-optimization //Optimised across files

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.


Esta resposta está desatualizada. A partir do Swift 4.1, a opção de otimização do módulo inteiro é um booleano separado que pode ser combinado com outras configurações e agora existe um -Os para otimizar o tamanho. Posso atualizar quando tiver tempo para verificar as opções exatas.
Joseph Lord

7
func partition(inout list : [Int], low: Int, high : Int) -> Int {
    let pivot = list[high]
    var j = low
    var i = j - 1
    while j < high {
        if list[j] <= pivot{
            i += 1
            (list[i], list[j]) = (list[j], list[i])
        }
        j += 1
    }
    (list[i+1], list[high]) = (list[high], list[i+1])
    return i+1
}

func quikcSort(inout list : [Int] , low : Int , high : Int) {

    if low < high {
        let pIndex = partition(&list, low: low, high: high)
        quikcSort(&list, low: low, high: pIndex-1)
        quikcSort(&list, low: pIndex + 1, high: high)
    }
}

var list = [7,3,15,10,0,8,2,4]
quikcSort(&list, low: 0, high: list.count-1)

var list2 = [ 10, 0, 3, 9, 2, 14, 26, 27, 1, 5, 8, -1, 8 ]
quikcSort(&list2, low: 0, high: list2.count-1)

var list3 = [1,3,9,8,2,7,5]
quikcSort(&list3, low: 0, high: list3.count-1) 

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.


4

O Swift 4.1 introduz um novo -Osizemodo de otimização.

No Swift 4.1, o compilador agora suporta um novo modo de otimização que permite otimizações dedicadas para reduzir o tamanho do código.

O compilador Swift vem com otimizações poderosas. Ao compilar com -O, o compilador tenta transformar o código para que seja executado com desempenho máximo. No entanto, esse aprimoramento no desempenho do tempo de execução pode vir às vezes com uma troca de tamanho de código aumentado. Com o novo modo de otimização -Osize, o usuário tem a opção de compilar para o tamanho mínimo do código, em vez da velocidade máxima.

Para ativar o modo de otimização de tamanho na linha de comando, use -Osize em vez de -O.

Leitura adicional: https://swift.org/blog/osize/

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.