2

The following program in C# computes 10 million Babylonian iterations for the square root.

using System;
using System.Diagnostics;

namespace Performance {

    public class Program {
        public static void MeasureTime(long n, Action f) {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (long i = 0; i < n; ++i) f();
            watch.Stop();
            Console.WriteLine($"{(n / watch.ElapsedMilliseconds) / 1000} Mop/s, {watch.ElapsedMilliseconds} ms");
        }

        public static void TestSpeed(double a) {
            Console.WriteLine($"Parameter {a}");
            double x = a;
            long n = 10_000_000;
            MeasureTime(n, () => x = (a / x + x) / 2);
            Console.WriteLine($"{x}\n");
        }

        static void Main(string[] args) {
            TestSpeed(2);
            TestSpeed(Double.PositiveInfinity);
        }
    }

}

When I run this on my computer in Release mode, I get:

Parameter 2
99 Mop/s, 101 ms
1,41421356237309

Parameter ∞
3 Mop/s, 3214 ms
NaN

Here Mop/s stands for million operations per second. When the parameter is infinity, for some reason the code slows down more than 30x.

Why is this?

For comparison, here is the same program written in C++20:

#include <iostream>
#include <chrono>
#include <format>

namespace Performance {

    template <typename F>
    void MeasureTime(long long n, F f) {
        auto begin = std::chrono::steady_clock::now();
        for (long long i = 0; i < n; ++i) f();
        auto end = std::chrono::steady_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
        std::cout << std::format("{0} Mop/s, {1} ms", (n / ms) / 1000, ms) << std::endl;
    }

    void TestSpeed(double a) {
        std::cout << std::format("Parameter {0}", a) << std::endl;
        double x = a;
        long long n = 10'000'000;
        MeasureTime(n, [&]() { x = (a / x + x) / 2; });
        std::cout << std::format("{0}\n\n", x);
    }

}

using namespace Performance;

int main() {
    auto inf = std::numeric_limits<double>::infinity();
    TestSpeed(2);
    TestSpeed(inf);
    return 0;
}

When I run this program in Release mode, I get:

Parameter 2
181 Mop/s, 55 ms
1.414213562373095

Parameter inf
192 Mop/s, 52 ms
-nan(ind)

Which is as expected; i.e. no difference in performance.

Both programs are built in Visual Studio 2022 version 17.1.0. C# project is a Net Framework 4.7.2 Console Application.

kaba
  • 703
  • 6
  • 17
  • This is only reproduced with debug configuration. – shingo Feb 24 '22 at 07:20
  • For me this happens with the default Release configuration. There seems to be something specific to my environment or machine, since when I run it on various online C# compilers, it runs fine. – kaba Feb 24 '22 at 07:28
  • In the options there is an option "Prefer 32 bits". When that option is unchecked, the problem is fixed! I wonder what is going on. – kaba Feb 24 '22 at 07:44
  • 2
    `fdiv` and `fadd` are used in a 32bits+debug environment. They seem slow to compute NaN numbers. [This answer](https://stackoverflow.com/a/31879376/6196568) contains some details. – shingo Feb 24 '22 at 08:24
  • On the other hand, building the C++ program in 32-bit mode does not affect its performance at all. – kaba Feb 24 '22 at 08:50
  • @shingo I think you were right. I was able to reproduce the problem in C++ side. See edits. – kaba Feb 24 '22 at 09:12

1 Answers1

1

The problem was fixed by unchecking Prefer 32-bits in the C# project options.

I was also able to reproduce the performance problem on C++ side by changing the Enable Enhanced Instruction Set option in Visual Studio to either No Enhanced Instructions (/arch:IA32) or Streaming SIMD Extensions (/arch:SSE). These options are only available when building a 32-bit program. As was hinted by @shingo in the comments, there seems to be a performance problem when computing with NaNs in older 32-bit instruction sets. Indeed, the given code computes solely with NaNs when the parameter a is set to infinity.

kaba
  • 703
  • 6
  • 17