为什么此 Python 代码比等效的 Clojure 代码更快

Why is this Python code faster than its equivalent Clojure code

有人告诉我,我相信 Clojure 比 Python 快。为什么这个 Python 代码 运行 比这个看似等效的 Clojure 代码更快? Python 是否在编译时进行了一些优化?

def find_fifty(n,memory=1,count=0):
    if memory < 0.5:
        return count
    else:
        return find_fifty(n,memory*(1 - count/n),count+1)

find_fifty(100000) 
(defn fifty 
    ([n] (fifty n 1 0))
    ([n memory count]
        (if (< memory 0.5)
            count
            (recur n
                   (* memory (- 1 (/ count n)))
                   (inc count)))))

(fifty 100000)

感觉Clojure 的时间复杂度比Python 高。 Python 函数可以接收比 Clojure 函数高很多倍的输入,然后它在 运行 时间上有显着增加。

更新 - Clojure 修复

(defn fifty 
    ([n] (fifty (float n) 1 0))
    ([n memory count]
        (if (< memory 0.5)
            count
            (recur n
                   (* memory (- 1 (/ count n)))
                   (inc count)))))

(fifty 10000000)

正如回答的那样,Clojure 将值保持为非常大的有理数。将其转换为浮点数可简化正在完成的操作,从而减少 运行时间。

Clojure 的除法运算符在应用于整数时进行精确的有理除法。它不像 Python 那样四舍五入到下一个最小整数。你的算法涉及 memory 成为一个非常复杂的分数,尽管产生一个简单的整数。

我修改了你的函数以在最终返回结果之前打印它的中间值:

(defn fifty
  ([n] (fifty n 1 0))
  ([n memory count]
    (if (< memory 0.5) 
      count 
      (do (println n (* memory (- 1 (/ count n))) (inc count)) 
          (fifty n (* memory (- 1 (/ count n))) (inc count))))))

结果如下:

(fifty 500)
500 1 1
500 499/500 2
500 124251/125000 3
500 61752747/62500000 4
500 1914335157/1953125000 5
500 189519180543/195312500000 6
500 46811237594121/48828125000000 7
500 23077940133901653/24414062500000000 8
500 2838586636469903319/3051757812500000000 9
500 1393746038506722529629/1525878906250000000000 10
500 68293555886829403951821/76293945312500000000000 11
500 33395548828659578532440469/38146972656250000000000000 12
500 2037128478548234290478868609/2384185791015625000000000000 13
500 992081569052990099463209012583/1192092895507812500000000000000 14
500 241075821279876594169559790057669/298023223876953125000000000000000 15
500 23384354664148029634447299635593893/29802322387695312500000000000000000 16
500 2829506914361911585768123255906861053/3725290298461914062500000000000000000 17
500 1366651839636803295926003532603013888599/1862645149230957031250000000000000000000 18
500 329363093352469594318166851357326347152359/465661287307739257812500000000000000000000 19
500 158423647902537874867038255502873972980284679/232830643653869628906250000000000000000000000 20
500 475270943707613624601114766508621918940854037/727595761418342590332031250000000000000000000 21
500 227654782035946926183933973157629899172669083723/363797880709171295166015625000000000000000000000 22
500 54409492906591315357960219584673545902267911009797/90949470177292823791503906250000000000000000000000 23
500 25953328116444057425747024741889281395381793551673169/45474735088646411895751953125000000000000000000000000 24
500 3088446045856842833663895944284824486050433432649107111/5684341886080801486968994140625000000000000000000000000 25
500 58680474871280013839614022941411665234958235220333035109/113686837721616029739379882812500000000000000000000000000 26
500 13907272544493363279988523437114564660685101747218929320833/28421709430404007434844970703125000000000000000000000000000 27
27

我希望你能看到即使是很小的输入,计算这些不合理的分数也是多么昂贵。如果您想要与 Python 版本相同的逻辑,您可以简单地将 / 替换为 quot.

一些建议:

OP 或@amalloy 的函数 fiftyn 参数未被 recur 更改。所以最好用 loop:

(defn fifty [n]
  (loop [memory 1.0, count 0]
    (if (< memory 0.5)
      count
      (recur (* memory (- 1 (/ count n))) (inc count)))))

通过将 memory 的初始值设置为 1.0 而不是 1* 会产生一系列浮点数(准确地说是双精度数),而不是理性。

我们可以使用序列函数让事情变得更清楚:

(defn fifty- [n]
  (let [factors (map (fn [count] (- 1 (/ count n))) (range))
        products (reductions * 1.0 factors)]
    (count (take-while #(< 0.5 %) products))))

这可能是最慢的。