3

(There has some Edit in below)
Well, I wrote exactly the same code with Swift and C lang. It's a code to find a Prime number and show that.
I expect that Swift lang's Code is much faster than C lang's program, but It doesn't.

Is there any reason Swift lang is much slower than C lang code?

When I found until 4000th Prime number, C lang finished calculating with only one second. But, Swift finished with 38.8 seconds. It's much much slower than I thought.

Here is a code I wrote.

Do there any solutions to fast up Swift's code? (Sorry for the Japanese comment or text in the code.)

Swift

import CoreFoundation
/*
var calendar = Calendar.current
calender.locale = .init(identifier: "ja.JP")
*/

var primeCandidate: Int
var prime: [Int] = []

var countMax: Int

print("いくつ目まで?(最小2、最大100000まで)\n→ ", terminator: "")

countMax = Int(readLine()!)!

var flagPrint: Int

print("表示方法を選んでください。(1:全て順番に表示、2:\(countMax)番目の一つだけ表示)\n→ ", terminator: "")
flagPrint = Int(readLine()!)!

prime.append(2)
prime.append(3)

var currentMaxCount: Int = 2
var numberCount: Int

primeCandidate = 4

var flag: Int = 0
var ix: Int

let startedTime = clock()
//let startedTime = time()
//.addingTimeInterval(0.0)

while currentMaxCount < countMax {
    for ix in 2..<primeCandidate {
        if primeCandidate % ix == 0 {
            flag = 1
            break
        }
    }
    
    if flag == 0 {
        prime.append(primeCandidate)
        currentMaxCount += 1
    } else if flag == 1 {
        flag = 0
    }
    
    primeCandidate += 1
}

let endedTime = clock()
//let endedTime = Time()
//.timeIntervalSince(startedTime)

if flagPrint == 1 {
    print("計算された素数の一覧:", terminator: "")
    
    let completedPrimeNumber = prime.map {
        $0
    }
    
    
    print(completedPrimeNumber)
    //print("\(prime.map)")
    
    print("\n\n終わり。")
    
} else if flagPrint == 2 {
    print("\(currentMaxCount)番目の素数は\(prime[currentMaxCount - 1])です。")
}

print("\(countMax)番目の素数まで計算。")
print("計算経過時間: \(round(Double((endedTime - startedTime) / 100000)) / 10)秒")

Clang

#include <stdio.h>
#include <time.h> //経過時間計算のため

int main(void)
{
    int primeCandidate;
    unsigned int prime[100000];
    
    int countMax;
    
    printf("いくつ目まで?(最小2、最大100000まで)\n→ ");
    scanf("%d", &countMax);
    
    int flagPrint;
    
    printf("表示方法を選んでください。(1:全て順番に表示、2:%d番目の一つだけ表示)\n→ ", countMax);
    scanf("%d", &flagPrint);
    
    prime[0] = 2;
    prime[1] = 3;
    
    int currentMaxCount = 2;
    int numberCount;
    
    primeCandidate = 4;
    
    int flag = 0;
    
    int ix;
    
    int startedTime = time(NULL);
    for(;currentMaxCount < countMax;primeCandidate++){
        /*
        for(numberCount = 0;numberCount < currentMaxCount - 1;numberCount++){
            if(primeCandidate % prime[numberCount] == 0){
                flag = 1;
                break;
            }
        }
            */
            
        for(ix = 2;ix < primeCandidate;++ix){
            if(primeCandidate % ix == 0){
                flag = 1;
                break;
            }
        }
            
        if(flag == 0){
            prime[currentMaxCount] = primeCandidate;
            currentMaxCount++;
        } else if(flag == 1){
            flag = 0;
        }
    }
    int endedTime = time(NULL);
    
    if(flagPrint == 1){
        printf("計算された素数の一覧:");
        for(int i = 0;i < currentMaxCount - 1;i++){
            printf("%d, ", prime[i]);
        }
        printf("%d.\n\n終わり", prime[currentMaxCount - 1]);
    } else if(flagPrint == 2){
        printf("%d番目の素数は「%d」です。\n",currentMaxCount ,prime[currentMaxCount - 1]);
    }
    
    printf("%d番目の素数まで計算", countMax);
    printf("計算経過時間: %d秒\n", endedTime - startedTime);
    
    return 0;
}


**Add**
I found some reason for one.
for ix in 0..<currentMaxCount - 1 {
        if primeCandidate % prime[ix] == 0 {
            flag = 1
            break
        }
    }

I wrote a code to compare all numbers. That was a mistake. But, I fix with code with this, also Swift finished calculating in 4.7 secs. It's 4 times slower than C lang also.
Luna
  • 43
  • 7
  • 2
    Why would you expect swift to be faster than C? – Stephen Newell Jul 11 '22 at 01:24
  • Because Swift is announced much later than C lang. – Luna Jul 11 '22 at 01:27
  • And also, Swift says that is much faster than Obj-C. "According to Apple, Swift is 2.6 times faster than Objective-C. The fact may be that Swift was created as a new language in order to be “Swift.”" So I thought that is faster than C lang too. – Luna Jul 11 '22 at 01:29
  • 1
    Objective-C and C are different languages, albeit closely related. I don't know Swift and not sure what Apple did to get their benchmarks, but it's usually pretty hard to beat C in raw performance. – Stephen Newell Jul 11 '22 at 01:37
  • I see... I didn't expect that. thank you for your comment! – Luna Jul 11 '22 at 01:44
  • 2
    I removed the timers and the printing code and left the algorithm intact. The assembly output from swift is about 10x larger than the C code output. https://godbolt.org/z/P93hGcjGG Smaller doesn't necessarily mean faster but you can look at the assembly to see why and where swift is slower. – Bill Morgan Jul 11 '22 at 01:44
  • Swift's 'append()' is much larger than C lang's array(number[] = )... I think that is also a reason for one. Also, Swift's while / for is much larger than C lang... I really didn't expect that. I really thank you for your comment. I learn a lot! – Luna Jul 11 '22 at 01:52
  • 2
    @BillMorgan None of those programs are compiled with optimization enabled, which makes the resulting binaries completely useless for performance comparison. Debug builds optimize for fast compile time (for a faster developer experience), with no regard for the performance of the result, apart from optimizing the lowest of low hanging fruit. – Alexander Jul 11 '22 at 01:53
  • 1
    @Luna "Swift's 'append()' is much larger than C lang's array(number[] = )... I think that is also a reason for one." Indeed, the Swift `Array` is a [dynamic array](https://en.wikipedia.org/wiki/Dynamic_array) that dynamically resizes to fit new content that you append to it. It tracks its own count, capacity, and does bounds checks on every `append`. Each time it runs out of space, it needs to reallocate into a larger buffer, which typically involves copying the full previous buffer. This reallocation can happen many times in your program, and it's often avoidable. ... – Alexander Jul 11 '22 at 01:57
  • 1
    @Luna see [`Array.reserveCapacity(_:)`](https://developer.apple.com/documentation/swift/array/reservecapacity(_:)-5cknc) – Alexander Jul 11 '22 at 01:57
  • 1
    @Luna Oh, each `for` loop in Swift calls `makeIterator()` on the `Sequence` being iterated, then calls `next()` on each iteration, then checks the result for `nil` to decide when to stop looping. All this function calling overhead can add up for the case of a typical looping over a range. This all gets inlined out once optimizations are enabled, so the result flattens down to be approximately equivalent to the simple loop-counter-increment and branching that the C for loop compiles to. – Alexander Jul 11 '22 at 02:09
  • 2
    By the way, merely turning on `-O` lowers the time on my M1 Mac Mini from 19.2 seconds to 0.00 seconds. – Alexander Jul 11 '22 at 02:19
  • Oh, I didn't know about `-O` option. It's much much faster on my MacBook Pro(Intel Core i9) too. Its time decreased 4.5 secs to 0.0 secs... – Luna Jul 11 '22 at 02:34

2 Answers2

5

The fundamental cause

As with most of these "why does this same program in 2 different languages perform differently?", the answer is almost always: "because they're not the same program."

They might be similar in high-level intent, but they're implemented differently enough that you can distinguish their performance.

Sometimes they're different in ways you can control (e.g. you use an array in one program and a hash set in the other) or sometimes in ways you can't (e.g. you're using CPython and you're experiencing the overhead of interpretation and dynamic method dispatch, as compared to compiled C function calls).

Some example differences

In this case, there's a few notable differences I can see:

  1. The prime array in your C code uses unsigned int, which is typically akin to UInt32. Your Swift code uses Int, which is typically equivalent to Int64. It's twice the size, which doubles memory usage and decreases the efficacy of the CPU cache.
  2. Your C code pre-allocates the prime array on the stack, whereas your Swift code starts with an empty Array, and repeatedly grows it as necessary.
  3. Your C code doesn't pre-initialize the contents of the prime array. Any junk that might be leftover in the memory is still there to be observed, whereas the Swift code will zero-out all the array memory before use.
  4. All Swift arithmetic operations are checked for overflow. This introduces a branch within every single +, %, etc. That's good for program safety (overflow bugs will never be silent and will always be detected), but sub-optimal in performance-critical code where you're certain that overflow is impossible. There's non-checked variants of all the operators that you can use, such as &+, &-, etc.

The general trend

In general, you'll notice a trend that Swift optimizes for safety and developer experience, whereas C optimizes for being close to the hardware. Swift optimizes for allowing the developer to express their intent about the business logic, whereas C optimizes for allowing the developer to express their intent about the final machine code that runs.

There are typically "escape hatches" in Swift that let you sacrifice safety or convenience for C-like performance. This sounds bad, but arguably, you can view C just being exclusively using these escape hatches. There's no Array, Dictionary, automatic reference counting, Sequence algorithms, etc. E.g. what Swift calls UnsafePointer is just a "pointer" in C. "Unsafe" comes with the territory.

Improving the performance

You could get pretty far in hitting performance parity by:

  1. Pre-allocating a sufficiently large array with [Array.reserveCapacity(_:)](https://developer.apple.com/documentation/swift/array/reservecapacity(_:)). See this note in the Array documentation:

    Growing the Size of an Array

    Every array reserves a specific amount of memory to hold its contents. When you add elements to an array and that array begins to exceed its reserved capacity, the array allocates a larger region of memory and copies its elements into the new storage. The new storage is a multiple of the old storage’s size. This exponential growth strategy means that appending an element happens in constant time, averaging the performance of many append operations. Append operations that trigger reallocation have a performance cost, but they occur less and less often as the array grows larger.

    If you know approximately how many elements you will need to store, use the reserveCapacity(_:) method before appending to the array to avoid intermediate reallocations. Use the capacity and count properties to determine how many more elements the array can store without allocating larger storage.

    For arrays of most Element types, this storage is a contiguous block of memory. For arrays with an Element type that is a class or @objc protocol type, this storage can be a contiguous block of memory or an instance of NSArray. Because any arbitrary subclass of NSArray can become an Array, there are no guarantees about representation or efficiency in this case.

  2. Use UInt32 or Int32 instead of Int.

  3. If necessary drop down to UnsafeMutableBuffer<UInt32> instead of Array<UInt32>. This is closer to the simple pointer implementation used in your C example.

  4. You can used unchecked arithmetic operators like &+, &-, &% and so on. Obviously, you should only do this when you're absolutely certain that overflow is impossible. Given how many thousands of silent overflow related bugs have come and gone, this is almost always a bad bet, but the loaded gun is available for you if you insist.

These aren't things you should generally do. They're merely possibilities that exist if they're necessary to improve performance of critical code.

For example, the Swift convention is to generally use Int unless you have a good reason to use something else. For example, Array.count returns an Int, even though it can never be negative, and is unlikely to ever need to be more than UInt32.max.

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • I understand it. thank you for the detailed answer! Swift is slower than C because Swift has lots of safety techniques... It was a blind spot. – Luna Jul 11 '22 at 02:01
  • 1
    @Luna Glad to help! I added even more details, to show you how you can sacrifice some safety for performance in places where it matters. Be warned, safety is important, and you should almost always keep it unless you have a *really* good reason not to. – Alexander Jul 11 '22 at 02:06
  • 1
    I tried a solution. There has improved 0.2 sec ~ 0.4 sec faster than the original code. (the original was 4.7 secs) I didn't try UnsafeMutableBufferPointer cause I probably have to change the code a lot, but I understood about your solution. – Luna Jul 11 '22 at 02:28
5

You've forgotten to turn on the optimizer. Swift is much slower without optimization than C, but on things like this is roughly the same when optimized:

➜  x swift -O prime.swift
いくつ目まで?(最小2、最大100000まで)
→ 40000
表示方法を選んでください。(1:全て順番に表示、2:40000番目の一つだけ表示)
→ 2
40000番目の素数は479909です。
40000番目の素数まで計算。
計算経過時間: 5.9秒
➜  x clang -O3 prime.c && ./a.out
いくつ目まで?(最小2、最大100000まで)
→ 40000
表示方法を選んでください。(1:全て順番に表示、2:40000番目の一つだけ表示)
→ 2
40000番目の素数は「479909」です。
40000番目の素数まで計算計算経過時間: 6秒

This is without doing any work to improve your code (probably the most significant would be to pre-allocate the buffer like you do in C that doesn't actually matter).

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    I tried `-O` in edited code, and that time decreases to zero seconds! Thank you for the solution. :) – Luna Jul 11 '22 at 02:36