正弦计算比余弦慢几个数量级

sine calculation orders of magnitude slower than cosine

tl;博士

同一个numpy数组,计算np.cos需要3.2秒,而np.sin运行s 548秒(九分钟) Linux 完好

完整代码请参阅 this repo


我有一个脉冲信号(见下图),我需要将其调制到 HF 载波上,模拟 Laser Doppler Vibrometer。因此需要对信号及其时基进行重采样以匹配载波的更高采样率。

在接下来的解调过程中,同相载波cos(omega * t)和移相载波sin(omega * t)都需要。 奇怪的是,评估这些函数的时间在很大程度上取决于计算时间向量的方式。

时间向量 t1 直接使用 np.linspace 计算,t2 使用 method implemented in scipy.signal.resample.

pulse = np.load('data/pulse.npy')  # 768 samples

pulse_samples = len(pulse)
pulse_samplerate = 960  # 960 Hz
pulse_duration = pulse_samples / pulse_samplerate  # here: 0.8 s
pulse_time = np.linspace(0, pulse_duration, pulse_samples,
                         endpoint=False)

carrier_freq = 40e6  # 40 MHz
carrier_samplerate = 100e6  # 100 MHz
carrier_samples = pulse_duration * carrier_samplerate  # 80 million

t1 = np.linspace(0, pulse_duration, carrier_samples)

# method used in scipy.signal.resample
# https://github.com/scipy/scipy/blob/v0.17.0/scipy/signal/signaltools.py#L1754
t2 = np.arange(0, carrier_samples) * (pulse_time[1] - pulse_time[0]) \
        * pulse_samples / float(carrier_samples) + pulse_time[0]

如下图所示,时间向量并不相同。在 8000 万个样本中,差异 t1 - t2 达到 1e-8

计算 t1 的同相和偏移载波在我的机器上每次需要 3.2 秒
然而,使用 t2,计算移位的载波需要 540 秒。九分钟。对于几乎相同的 8000 万个值。

omega_t1 = 2 * np.pi * carrier_frequency * t1
np.cos(omega_t1)  # 3.2 seconds
np.sin(omega_t1)  # 3.3 seconds

omega_t2 = 2 * np.pi * carrier_frequency * t2
np.cos(omega_t2)  # 3.2 seconds
np.sin(omega_t2)  # 9 minutes

我可以在我的 32 位笔记本电脑和 64 位塔式电脑上重现这个错误,两者都是 运行ning Linux Mint 17。然而,在我室友的 MacBook 上,"slow sine" 花费的时间与其他三个计算一样少。


I 运行 a Linux Mint 17.03 在 64 位 AMD 处理器上和 Linux Mint 17.2 在 32 位 Intel 处理器上。

我认为 numpy 与此无关:我认为您遇到了系统上 C 数学库中的性能错误,该错误会影响 pi 的大倍数附近的 sin。 (我在这里在相当广泛的意义上使用“bug”——据我所知,由于大浮点数的正弦定义不明确,“bug”实际上是库正确处理极端情况的行为!)

在 linux,我得到:

>>> %timeit -n 10000 math.sin(6e7*math.pi)
10000 loops, best of 3: 191 µs per loop
>>> %timeit -n 10000 math.sin(6e7*math.pi+0.12)
10000 loops, best of 3: 428 ns per loop

和其他 Linux- 使用来自 Python chatroom 报告的类型

10000 loops, best of 3: 49.4 µs per loop 
10000 loops, best of 3: 206 ns per loop

In [3]: %timeit -n 10000 math.sin(6e7*math.pi)
10000 loops, best of 3: 116 µs per loop

In [4]: %timeit -n 10000 math.sin(6e7*math.pi+0.12)
10000 loops, best of 3: 428 ns per loop

但是 Mac 用户报告

In [3]: timeit -n 10000 math.sin(6e7*math.pi)
10000 loops, best of 3: 300 ns per loop

In [4]: %timeit -n 10000 math.sin(6e7*math.pi+0.12)
10000 loops, best of 3: 361 ns per loop

没有数量级的差异。作为解决方法,您可以先尝试取 mod 2 pi:

>>> new = np.sin(omega_t2[-1000:] % (2*np.pi))
>>> old = np.sin(omega_t2[-1000:])
>>> abs(new - old).max()
7.83773902468434e-09

哪个性能更好:

>>> %timeit -n 1000 new = np.sin(omega_t2[-1000:] % (2*np.pi))
1000 loops, best of 3: 63.8 µs per loop
>>> %timeit -n 1000 old = np.sin(omega_t2[-1000:])
1000 loops, best of 3: 6.82 ms per loop

请注意,正如预期的那样,cos 会发生类似的效果,只是移位:

>>> %timeit -n 1000 np.cos(6e7*np.pi + np.pi/2)
1000 loops, best of 3: 37.6 µs per loop
>>> %timeit -n 1000 np.cos(6e7*np.pi + np.pi/2 + 0.12)
1000 loops, best of 3: 2.46 µs per loop

造成这些巨大性能差异的一个可能原因可能是数学库如何创建或处理 IEEE 浮点下溢(或非范数),这可能是由超越函数逼近过程中一些更小的尾数位的差异产生的.你的 t1 和 t2 向量可能因这些较小的尾数位而不同,以及用于计算你链接的任何库中的超越函数的算法,以及每个特定 OS 上的 IEEE 算术 denorms 或下溢处理程序。