เปรียบเทียบความเร็วกับ Project Euler: C กับ Python เทียบกับ Erlang และ Haskell


672

ฉันได้นำปัญหา # 12จากProject Euler มาเป็นแบบฝึกหัดการเขียนโปรแกรมและเพื่อเปรียบเทียบการใช้งานของฉันใน C, Python, Erlang และ Haskell เพื่อให้ได้เวลาดำเนินการที่สูงขึ้นฉันค้นหาหมายเลขสามเหลี่ยมแรกที่มีตัวหารมากกว่า 1,000 ตัวแทนที่จะเป็น 500 ตามที่ระบุในปัญหาดั้งเดิม

ผลที่ได้คือ:

ค:

lorenzo@enzo:~/erlang$ gcc -lm -o euler12.bin euler12.c
lorenzo@enzo:~/erlang$ time ./euler12.bin
842161320

real    0m11.074s
user    0m11.070s
sys 0m0.000s

งูหลาม:

lorenzo@enzo:~/erlang$ time ./euler12.py 
842161320

real    1m16.632s
user    1m16.370s
sys 0m0.250s

Python กับ PyPy:

lorenzo@enzo:~/Downloads/pypy-c-jit-43780-b590cf6de419-linux64/bin$ time ./pypy /home/lorenzo/erlang/euler12.py 
842161320

real    0m13.082s
user    0m13.050s
sys 0m0.020s

Erlang:

lorenzo@enzo:~/erlang$ erlc euler12.erl 
lorenzo@enzo:~/erlang$ time erl -s euler12 solve
Erlang R13B03 (erts-5.7.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.4  (abort with ^G)
1> 842161320

real    0m48.259s
user    0m48.070s
sys 0m0.020s

Haskell:

lorenzo@enzo:~/erlang$ ghc euler12.hs -o euler12.hsx
[1 of 1] Compiling Main             ( euler12.hs, euler12.o )
Linking euler12.hsx ...
lorenzo@enzo:~/erlang$ time ./euler12.hsx 
842161320

real    2m37.326s
user    2m37.240s
sys 0m0.080s

สรุป:

  • C: 100%
  • Python: 692% (118% กับ PyPy)
  • Erlang: 436% (135% ขอบคุณ RichardC)
  • Haskell: 1421%

ฉันคิดว่า C มีข้อได้เปรียบที่ยิ่งใหญ่เนื่องจากมันใช้เวลานานในการคำนวณและไม่ใช่จำนวนเต็มที่มีความยาวตามอำเภอใจเหมือนอีกสามตัว นอกจากนี้ไม่จำเป็นต้องโหลดรันไทม์ก่อน (ทำอย่างอื่นหรือไม่)

คำถามที่ 1: Erlang, Python และ Haskell สูญเสียความเร็วเนื่องจากการใช้จำนวนเต็มความยาวตามอำเภอใจหรือไม่ตราบเท่าที่ค่าน้อยกว่าMAXINT?

คำถามที่ 2: ทำไม Haskell ถึงช้าขนาดนี้ มีธงคอมไพเลอร์ที่ปิดเบรกหรือเป็นการติดตั้งของฉันหรือไม่? (อันหลังเป็นไปได้ค่อนข้างมากเนื่องจาก Haskell เป็นหนังสือที่มีตราประทับเจ็ดเล่มให้ฉัน)

คำถามที่ 3: คุณสามารถให้คำแนะนำกับฉันเกี่ยวกับวิธีเพิ่มประสิทธิภาพการใช้งานเหล่านี้ได้โดยไม่เปลี่ยนวิธีที่ฉันใช้พิจารณาปัจจัยต่างๆ การเพิ่มประสิทธิภาพในทุกวิถีทาง: ดีกว่าเร็วขึ้นมากขึ้น "เป็นเจ้าของภาษา"

แก้ไข:

คำถามที่ 4: การใช้งานฟังก์ชั่นของฉันอนุญาต LCO (การเพิ่มประสิทธิภาพการโทรล่าสุด, การกำจัดหางแบบเรียกซ้ำ) หรือไม่และเพื่อหลีกเลี่ยงการเพิ่มเฟรมที่ไม่จำเป็นลงในสแต็กการโทร?

ฉันพยายามใช้อัลกอริทึมแบบเดียวกันกับที่ใกล้เคียงที่สุดในสี่ภาษาแม้ว่าฉันต้องยอมรับว่าความรู้ของ Haskell และ Erlang นั้นมี จำกัด มาก


รหัสที่มาใช้:

#include <stdio.h>
#include <math.h>

int factorCount (long n)
{
    double square = sqrt (n);
    int isquare = (int) square;
    int count = isquare == square ? -1 : 0;
    long candidate;
    for (candidate = 1; candidate <= isquare; candidate ++)
        if (0 == n % candidate) count += 2;
    return count;
}

int main ()
{
    long triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index ++;
        triangle += index;
    }
    printf ("%ld\n", triangle);
}

#! /usr/bin/env python3.2

import math

def factorCount (n):
    square = math.sqrt (n)
    isquare = int (square)
    count = -1 if isquare == square else 0
    for candidate in range (1, isquare + 1):
        if not n % candidate: count += 2
    return count

triangle = 1
index = 1
while factorCount (triangle) < 1001:
    index += 1
    triangle += index

print (triangle)

-module (euler12).
-compile (export_all).

factorCount (Number) -> factorCount (Number, math:sqrt (Number), 1, 0).

factorCount (_, Sqrt, Candidate, Count) when Candidate > Sqrt -> Count;

factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;

factorCount (Number, Sqrt, Candidate, Count) ->
    case Number rem Candidate of
        0 -> factorCount (Number, Sqrt, Candidate + 1, Count + 2);
        _ -> factorCount (Number, Sqrt, Candidate + 1, Count)
    end.

nextTriangle (Index, Triangle) ->
    Count = factorCount (Triangle),
    if
        Count > 1000 -> Triangle;
        true -> nextTriangle (Index + 1, Triangle + Index + 1)  
    end.

solve () ->
    io:format ("~p~n", [nextTriangle (1, 1) ] ),
    halt (0).

factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
    where square = sqrt $ fromIntegral number
          isquare = floor square

factorCount' number sqrt candidate count
    | fromIntegral candidate > sqrt = count
    | number `mod` candidate == 0 = factorCount' number sqrt (candidate + 1) (count + 2)
    | otherwise = factorCount' number sqrt (candidate + 1) count

nextTriangle index triangle
    | factorCount triangle > 1000 = triangle
    | otherwise = nextTriangle (index + 1) (triangle + index + 1)

main = print $ nextTriangle 1 1

55
@Jochen (และ Seth) ไม่ใช่ว่า C นั้นเร็วหรือยอดเยี่ยม แต่มันก็ถูกมองว่าเป็นเรื่องง่ายที่จะเขียนโค้ดนักแสดง (ซึ่งอาจไม่เป็นความจริง ขณะที่ฉันสำรวจในคำตอบของฉันและพบว่าเป็นจริงตลอดเวลาทักษะโปรแกรมเมอร์และความรู้ของการเพิ่มประสิทธิภาพทั่วไปสำหรับภาษาที่เลือกมีความสำคัญมาก (โดยเฉพาะอย่างยิ่งสำหรับ Haskell)
Thomas M. DuBuisson

52
เพียงแค่ตรวจสอบกับมาติกา - มันจะใช้เวลา 0.25sec (กับ C ก็จะใช้เวลา 6sec ที่นี่) Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]และรหัสที่เป็นเพียง: เย่!
tsvikas

35
มีใครอีกบ้างที่จำสงครามเหล่านี้ระหว่างซีและการประชุม? "แน่นอน! คุณสามารถเขียนรหัสของคุณได้เร็วขึ้น 10 เท่าใน C แต่รหัส C ของคุณสามารถทำงานได้อย่างรวดเร็วนี้หรือไม่? ... " ฉันแน่ใจว่าการต่อสู้แบบเดียวกันได้เกิดขึ้นระหว่างเครื่องรหัสและชุดประกอบ
JS

39
@JS: อาจไม่ได้เนื่องจากชุดประกอบเป็นเพียงชุดคำย่อที่คุณพิมพ์แทนรหัสเครื่องไบนารี่ดิบ - โดยทั่วไปจะมีการโต้ตอบกันระหว่าง 1-1
Callum Rogers

9
ข้อสรุปสำหรับ Haskell: -O2 ให้ความเร็วประมาณ 3 เท่าและใช้ Int แทน Integer ประมาณ 4x-6x สำหรับการเร่งความเร็วโดยรวม 12x-14x และอื่น ๆ
Will Ness

คำตอบ:


794

การใช้GHC 7.0.3, gcc 4.4.6, Linux 2.6.29บน x86_64 Core2 Duo (2.5GHz) เครื่องรวบรวมใช้ghc -O2 -fllvm -fforce-recompสำหรับ Haskell และgcc -O3 -lmสำหรับซี

  • รูทีน C ของคุณทำงานใน 8.4 วินาที (เร็วกว่าการวิ่งของคุณอาจเป็นเพราะ-O3)
  • โซลูชัน Haskell ทำงานใน 36 วินาที (เนื่องจากการ-O2ตั้งค่าสถานะ)
  • factorCount'รหัสของคุณไม่ได้พิมพ์อย่างชัดเจนและผิดนัดInteger(ขอบคุณแดเนียลสำหรับการวินิจฉัยผิดพลาดของฉันที่นี่!) ให้ลายเซ็นประเภทที่ชัดเจน (ซึ่งเป็นมาตรฐานการปฏิบัติอยู่แล้ว) โดยใช้Intและเวลาเปลี่ยนเป็น11.1 วินาที
  • ในการที่คุณได้เรียกว่าไม่มีความจำเป็นfactorCount' fromIntegralการแก้ไขจะไม่เปลี่ยนแปลงแม้ว่าคอมไพเลอร์จะฉลาด แต่โชคดีสำหรับคุณ
  • คุณใช้ในmodที่ที่remเร็วกว่าและเพียงพอ การเปลี่ยนแปลงนี้เวลาไป8.5 วินาที
  • factorCount'ใช้ข้อโต้แย้งพิเศษสองข้อที่ไม่เคยเปลี่ยน ( number, sqrt) อย่างต่อเนื่อง การเปลี่ยนแปลงของคนงาน / ผู้ห่อหุ้มทำให้เรา:
 $ time ./so
 842161320  

 real    0m7.954s  
 user    0m7.944s  
 sys     0m0.004s  

ที่เหมาะสม7.95 วินาที อย่างต่อเนื่องครึ่งวินาทีเร็วกว่าวิธีการแก้ปัญหาที่ C หากไม่มีการ-fllvmตั้งค่าสถานะฉันยังคงได้รับ8.182 secondsดังนั้นแบ็กเอนด์ NCG ก็ทำได้ดีในกรณีนี้เช่นกัน

สรุป: Haskell ยอดเยี่ยม

รหัสผลลัพธ์

factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
    where square = sqrt $ fromIntegral number
          isquare = floor square

factorCount' :: Int -> Int -> Int -> Int -> Int
factorCount' number sqrt candidate0 count0 = go candidate0 count0
  where
  go candidate count
    | candidate > sqrt = count
    | number `rem` candidate == 0 = go (candidate + 1) (count + 2)
    | otherwise = go (candidate + 1) count

nextTriangle index triangle
    | factorCount triangle > 1000 = triangle
    | otherwise = nextTriangle (index + 1) (triangle + index + 1)

main = print $ nextTriangle 1 1

แก้ไข: ดังนั้นเมื่อเราได้สำรวจแล้วให้ตอบคำถาม

คำถามที่ 1: erlang, python และ haskell สูญเสียความเร็วเนื่องจากการใช้จำนวนเต็มความยาวตามอำเภอใจหรือไม่ตราบเท่าที่ค่าน้อยกว่า MAXINT?

ใน Haskell การใช้Integerจะช้ากว่าIntแต่จะช้ากว่านั้นขึ้นอยู่กับการคำนวณ โชคดี (สำหรับเครื่อง 64 บิต) Intก็เพียงพอแล้ว เพื่อประโยชน์ในการพกพาคุณควรเขียนโค้ดของฉันใหม่เพื่อใช้งานInt64หรือWord64(C ไม่ใช่ภาษาเดียวที่มี a long)

คำถามที่ 2: ทำไมฮาเซลจึงช้าขนาดนี้? มีธงคอมไพเลอร์ที่ปิดเบรกหรือเป็นการติดตั้งของฉันหรือไม่? (อันหลังเป็นไปได้ค่อนข้างมากเพราะ Haskell เป็นหนังสือที่มีเจ็ดแมวน้ำให้ฉัน)

คำถามที่ 3: คุณสามารถให้คำแนะนำบางอย่างแก่ฉันถึงวิธีเพิ่มประสิทธิภาพการใช้งานเหล่านี้โดยไม่เปลี่ยนวิธีที่ฉันใช้พิจารณาปัจจัยหรือไม่ การเพิ่มประสิทธิภาพในทุกวิถีทาง: ดีกว่าเร็วขึ้นมากขึ้น "เป็นเจ้าของภาษา"

นั่นคือสิ่งที่ฉันตอบข้างต้น คำตอบคือ

  • 0) ใช้การเพิ่มประสิทธิภาพผ่าน -O2
  • 1) ใช้ประเภทที่รวดเร็ว (สะดุดตา: ไม่สามารถใช้กล่อง) ได้
  • 2) remไม่ใช่mod(การเพิ่มประสิทธิภาพที่ถูกลืมบ่อยครั้ง) และ
  • 3) การเปลี่ยนแปลงของผู้ปฏิบัติงาน / เสื้อคลุม (อาจเป็นการปรับให้เหมาะสมที่สุด)

คำถามที่ 4: การใช้งานฟังก์ชั่นของฉันอนุญาต LCO หรือไม่และหลีกเลี่ยงการเพิ่มเฟรมที่ไม่จำเป็นลงใน call stack?

ใช่นั่นไม่ใช่ปัญหา งานที่ดีและดีใจที่คุณพิจารณาเรื่องนี้


25
@Karl เพราะremจริง ๆ แล้วเป็นองค์ประกอบย่อยของการmodดำเนินการ (ไม่เหมือนกัน) หากคุณดูในไลบรารีฐาน GHC คุณจะเห็นmodการทดสอบหลายเงื่อนไขและปรับเครื่องหมายให้สอดคล้องกัน (ดูmodInt#ในBase.lhs)
โทมัสเอ็ม. DuBuisson

20
จุดข้อมูลอื่น: ฉันเขียนการแปล Haskell อย่างรวดเร็วของโปรแกรม Cโดยไม่ได้ดู @ Hyperboreus's Haskell ดังนั้นมันจึงใกล้เคียงกับสำนวนมาตรฐาน Haskell และการเพิ่มประสิทธิภาพเพียงอย่างเดียวที่ฉันเพิ่มเข้ามาอย่างจงใจคือแทนที่modด้วยremหลังจากอ่านคำตอบนี้ (heh, oops) ดูลิงค์สำหรับการกำหนดเวลาของฉัน แต่เวอร์ชันย่อคือ "เกือบจะเหมือนกับ C"
CA McCann

106
แม้จะคิดว่าเวอร์ชั่น C ทำงานได้เร็วขึ้นบนเครื่องของฉันฉันก็มีความเคารพใหม่สำหรับ Haskell ในตอนนี้ +1
Seth Carnegie

11
นี่เป็นเรื่องที่ค่อนข้างแปลกสำหรับฉันแม้ว่าฉันจะยังไม่ได้ลอง เนื่องจากต้นฉบับfactorCount'เป็นแบบเรียกซ้ำแบบหางฉันจะคิดว่าคอมไพเลอร์สามารถมองเห็นพารามิเตอร์พิเศษที่ไม่เปลี่ยนแปลงและปรับการเรียกซ้ำแบบหางให้เหมาะสมที่สุดสำหรับพารามิเตอร์ที่เปลี่ยนแปลงเท่านั้น (Haskell เป็นภาษาบริสุทธิ์หลังจากนี้ทั้งหมดน่าจะง่าย) ทุกคนคิดว่าคอมไพเลอร์สามารถทำเช่นนั้นหรือฉันควรกลับไปอ่านเอกสารทฤษฎีเพิ่มเติมหรือไม่
kizzx2

22
@ kizzx2: มีตั๋ว GHC ที่จะเพิ่มไว้ จากสิ่งที่ฉันเข้าใจการเปลี่ยนแปลงนี้อาจส่งผลให้มีการจัดสรรวัตถุปิดเพิ่มเติมเพิ่มเติม นี่หมายถึงประสิทธิภาพที่แย่ลงในบางกรณี แต่อย่างที่ Johan Tibell แนะนำในโพสต์บล็อกของเขาสิ่งนี้สามารถหลีกเลี่ยงได้หากกระดาษห่อที่เกิดขึ้นสามารถถูกแทรกได้
hammar

224

มีปัญหาบางอย่างกับการใช้ Erlang พื้นฐานสำหรับสิ่งต่อไปนี้เวลาดำเนินการที่วัดได้ของฉันสำหรับโปรแกรม Erlang ที่ไม่ได้แก้ไขของคุณคือ 47.6 วินาทีเมื่อเทียบกับ 12.7 วินาทีสำหรับรหัส C

สิ่งแรกที่คุณควรทำถ้าคุณต้องการเรียกใช้รหัส Erlang ที่คำนวณโดยใช้คอมพิวเตอร์คือการใช้รหัสดั้งเดิม การคอมไพล์ด้วยerlc +native euler12ทำให้มีเวลาลงถึง 41.3 วินาที อย่างไรก็ตามนี่เป็นการเพิ่มความเร็วที่ต่ำกว่ามาก (เพียง 15%) จากที่คาดไว้จากการคอมไพล์ดั้งเดิมของโค้ดประเภทนี้และปัญหาก็คือการใช้งานของ-compile(export_all)คุณ สิ่งนี้มีประโยชน์สำหรับการทดลอง แต่ความจริงที่ว่าฟังก์ชั่นทั้งหมดนั้นสามารถเข้าถึงได้จากภายนอกทำให้คอมไพเลอร์เนทีฟมีความระมัดระวังมาก (ตัวจำลอง BEAM ปกติไม่ได้รับผลกระทบมากนัก) การแทนที่การประกาศนี้-export([solve/0]).จะให้ความเร็วที่ดีกว่า: 31.5 วินาที (เกือบ 35% จากพื้นฐาน)

แต่รหัสนั้นมีปัญหา: สำหรับการวนซ้ำแต่ละครั้งในวน factorCount คุณทำการทดสอบนี้:

factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;

รหัส C ไม่ได้ทำเช่นนี้ โดยทั่วไปอาจเป็นการยากที่จะทำการเปรียบเทียบอย่างเป็นธรรมระหว่างการใช้งานที่แตกต่างกันของรหัสเดียวกันและโดยเฉพาะอย่างยิ่งหากอัลกอริทึมนั้นเป็นตัวเลขเพราะคุณต้องแน่ใจว่าพวกเขากำลังทำสิ่งเดียวกันจริง ๆ ข้อผิดพลาดในการปัดเศษเล็กน้อยในการนำไปใช้งานอย่างหนึ่งเนื่องจาก typecast บางแห่งอาจทำให้การทำซ้ำหลายครั้งมากกว่าที่อื่นแม้ว่าทั้งคู่จะได้ผลลัพธ์เดียวกัน

เพื่อกำจัดแหล่งที่มาของข้อผิดพลาดที่เป็นไปได้ (และกำจัดการทดสอบเพิ่มเติมในการทำซ้ำแต่ละครั้ง) ฉันจะเขียนฟังก์ชัน factorCount ใหม่ดังต่อไปนี้สร้างแบบจำลองอย่างใกล้ชิดในรหัส C:

factorCount (N) ->
    Sqrt = math:sqrt (N),
    ISqrt = trunc(Sqrt),
    if ISqrt == Sqrt -> factorCount (N, ISqrt, 1, -1);
       true          -> factorCount (N, ISqrt, 1, 0)
    end.

factorCount (_N, ISqrt, Candidate, Count) when Candidate > ISqrt -> Count;
factorCount ( N, ISqrt, Candidate, Count) ->
    case N rem Candidate of
        0 -> factorCount (N, ISqrt, Candidate + 1, Count + 2);
        _ -> factorCount (N, ISqrt, Candidate + 1, Count)
    end.

การเรียบเรียงใหม่ไม่export_allและการรวบรวมดั้งเดิมทำให้ฉันใช้เวลาทำงานต่อไปนี้:

$ erlc +native euler12.erl
$ time erl -noshell -s euler12 solve
842161320

real    0m19.468s
user    0m19.450s
sys 0m0.010s

ซึ่งไม่เลวร้ายเกินไปเมื่อเทียบกับรหัส C:

$ time ./a.out 
842161320

real    0m12.755s
user    0m12.730s
sys 0m0.020s

การพิจารณาว่า Erlang นั้นไม่ได้มุ่งเน้นไปที่การเขียนโค้ดตัวเลข แต่ช้ากว่า C เพียงแค่ 50% ในโปรแกรมเช่นนี้ถือว่าค่อนข้างดี

ในที่สุดเกี่ยวกับคำถามของคุณ:

คำถามที่ 1: ทำ erlang, python และ haskell หลวมความเร็วเนื่องจากการใช้จำนวนเต็มความยาวโดยพลการหรือไม่พวกเขาตราบใดที่ค่าน้อยกว่า MAXINT?

ใช่แล้ว ใน Erlang ไม่มีวิธีพูดว่า "ใช้เลขคณิต 32/64 บิตที่มีการล้อมรอบ" ดังนั้นหากคอมไพเลอร์สามารถพิสูจน์ขอบเขตบนจำนวนเต็มของคุณ (และโดยปกติจะไม่สามารถทำได้) ต้องตรวจสอบการคำนวณทั้งหมดเพื่อดู ถ้าพวกเขาสามารถพอดีกับคำที่ติดแท็กเดียวหรือถ้ามันจะต้องเปลี่ยนเป็น bignums กองที่จัดสรร แม้ว่าจะไม่มีการใช้ bignums ในการฝึกซ้อมขณะใช้งานจริงก็ตามการตรวจสอบเหล่านี้จะต้องดำเนินการ ในทางตรงกันข้ามนั่นหมายความว่าคุณรู้ว่าอัลกอริธึมจะไม่ล้มเหลวเนื่องจากมีจำนวนเต็มที่ไม่คาดคิดหากคุณให้อินพุตที่ใหญ่กว่าเดิมทันที

คำถามที่ 4: การใช้งานฟังก์ชั่นของฉันอนุญาต LCO หรือไม่และหลีกเลี่ยงการเพิ่มเฟรมที่ไม่จำเป็นลงใน call stack?

ใช่รหัส Erlang ของคุณถูกต้องตามการเพิ่มประสิทธิภาพการโทรล่าสุด


2
ฉันเห็นด้วยกับคุณ. มาตรฐานนี้ไม่แม่นยำโดยเฉพาะสำหรับ Erlang ด้วยเหตุผลหลายประการ
Muzaaya Joshua

156

ในเรื่องเกี่ยวกับการปรับให้เหมาะสมของ Python นอกเหนือจากการใช้ PyPy (สำหรับการเพิ่มความเร็วที่น่าประทับใจโดยไม่มีการเปลี่ยนแปลงรหัสของคุณ) คุณสามารถใช้เครื่องมือการแปลของ PyPy เพื่อรวบรวมรุ่นที่สอดคล้องกับ RPython หรือCythonเพื่อสร้างโมดูลส่วนขยาย ซึ่งจะเร็วกว่ารุ่น C ในการทดสอบของฉันกับโมดูล Cython เกือบสองเท่าที่รวดเร็ว สำหรับการอ้างอิงฉันได้รวมผลลัพธ์มาตรฐาน C และ PyPy ไว้ด้วย:

C (รวบรวมด้วยgcc -O3 -lm)

% time ./euler12-c 
842161320

./euler12-c  11.95s 
 user 0.00s 
 system 99% 
 cpu 11.959 total

PyPy 1.5

% time pypy euler12.py
842161320
pypy euler12.py  
16.44s user 
0.01s system 
99% cpu 16.449 total

RPython (ใช้การแก้ไข PyPy ล่าสุดc2f583445aee)

% time ./euler12-rpython-c
842161320
./euler12-rpy-c  
10.54s user 0.00s 
system 99% 
cpu 10.540 total

Cython 0.15

% time python euler12-cython.py
842161320
python euler12-cython.py  
6.27s user 0.00s 
system 99% 
cpu 6.274 total

เวอร์ชัน RPython มีการเปลี่ยนแปลงที่สำคัญสองสามประการ ในการแปลเป็นโปรแกรมแบบสแตนด์อโลนคุณจำเป็นต้องกำหนดtargetซึ่งในกรณีนี้คือmainฟังก์ชั่น คาดว่าจะยอมรับได้sys.argvเนื่องจากเป็นอาร์กิวเมนต์เท่านั้นและจำเป็นต้องส่งคืนค่า int คุณสามารถแปลได้โดยใช้ translate.py % translate.py euler12-rpython.pyซึ่งแปลเป็น C และรวบรวมให้คุณ

# euler12-rpython.py

import math, sys

def factorCount(n):
    square = math.sqrt(n)
    isquare = int(square)
    count = -1 if isquare == square else 0
    for candidate in xrange(1, isquare + 1):
        if not n % candidate: count += 2
    return count

def main(argv):
    triangle = 1
    index = 1
    while factorCount(triangle) < 1001:
        index += 1
        triangle += index
    print triangle
    return 0

if __name__ == '__main__':
    main(sys.argv)

def target(*args):
    return main, None

รุ่น Cython ถูกเขียนใหม่เป็นโมดูลส่วนขยาย_euler12.pyxซึ่งฉันนำเข้าและโทรจากไฟล์ไพ ธ อนปกติ โดย_euler12.pyxพื้นฐานแล้วเหมือนกับเวอร์ชั่นของคุณพร้อมกับการประกาศประเภทสแตติกเพิ่มเติม setup.py python setup.py build_ext --inplaceมีต้นแบบปกติในการสร้างส่วนขยายที่ใช้

# _euler12.pyx
from libc.math cimport sqrt

cdef int factorCount(int n):
    cdef int candidate, isquare, count
    cdef double square
    square = sqrt(n)
    isquare = int(square)
    count = -1 if isquare == square else 0
    for candidate in range(1, isquare + 1):
        if not n % candidate: count += 2
    return count

cpdef main():
    cdef int triangle = 1, index = 1
    while factorCount(triangle) < 1001:
        index += 1
        triangle += index
    print triangle

# euler12-cython.py
import _euler12
_euler12.main()

# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("_euler12", ["_euler12.pyx"])]

setup(
  name = 'Euler12-Cython',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

ฉันมีประสบการณ์น้อยมากกับ RPython หรือ Cython อย่างจริงใจและรู้สึกประหลาดใจอย่างมากกับผลลัพธ์ที่ได้ หากคุณใช้ CPython การเขียนโค้ดที่ใช้ CPU มากในโมดูลส่วนขยาย Cython ดูเหมือนจะเป็นวิธีที่ง่ายที่สุดในการเพิ่มประสิทธิภาพโปรแกรมของคุณ


6
ฉันอยากรู้อยากเห็น C รุ่นสามารถปรับให้เร็วที่สุดเท่ากับ CPython อย่างน้อยได้หรือไม่
ชื่อที่ปรากฏ

4
@SargeBorsch ว่ารุ่น Cython นั้นเร็วมากเพราะมันรวบรวมแหล่ง C ที่ได้รับการปรับปรุงให้ดีที่สุดซึ่งหมายความว่าคุณจะได้ประสิทธิภาพที่ดีขึ้นจาก C
Eli Korvigo

72

คำถามที่ 3:คุณสามารถให้คำแนะนำกับฉันเกี่ยวกับวิธีเพิ่มประสิทธิภาพการใช้งานเหล่านี้ได้โดยไม่เปลี่ยนวิธีที่ฉันใช้พิจารณาปัจจัยต่างๆ การเพิ่มประสิทธิภาพในทุกวิถีทาง: ดีกว่าเร็วขึ้นมากขึ้น "เป็นเจ้าของภาษา"

การติดตั้ง C นั้นเป็นสิ่งที่ไม่ดี (ตามที่ Thomas M DuBuisson บอกไว้) รุ่นนี้ใช้จำนวนเต็ม 64- บิต (เช่นประเภทข้อมูลยาว ) ฉันจะตรวจสอบรายชื่อแอสเซมบลีในภายหลัง แต่ด้วยการเดาที่ได้รับการศึกษามีหน่วยความจำบางส่วนเกิดขึ้นในโค้ดที่คอมไพล์ซึ่งทำให้การใช้จำนวนเต็ม 64- บิตช้าลงอย่างมาก มันเป็นรหัสที่สร้างขึ้น (ไม่ว่าจะเป็นความจริงที่ว่าคุณสามารถใส่ 64- บิต ints น้อยลงในการลงทะเบียน SSE หรือปัดเศษเป็นสองเท่าเพื่อเป็นจำนวนเต็ม 64 บิตจะช้าลง)

นี่คือโค้ดที่ถูกแก้ไข (เพียงแค่แทนที่longด้วยintและฉัน Inline factorCount อย่างชัดเจนแม้ว่าฉันไม่คิดว่านี่เป็นสิ่งจำเป็นสำหรับ gcc -O3):

#include <stdio.h>
#include <math.h>

static inline int factorCount(int n)
{
    double square = sqrt (n);
    int isquare = (int)square;
    int count = isquare == square ? -1 : 0;
    int candidate;
    for (candidate = 1; candidate <= isquare; candidate ++)
        if (0 == n % candidate) count += 2;
    return count;
}

int main ()
{
    int triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index++;
        triangle += index;
    }
    printf ("%d\n", triangle);
}

การวิ่ง + เวลาจะให้:

$ gcc -O3 -lm -o euler12 euler12.c; time ./euler12
842161320
./euler12  2.95s user 0.00s system 99% cpu 2.956 total

สำหรับการอ้างอิงการใช้ Haskell โดย Thomas ในคำตอบก่อนหน้านี้ให้:

$ ghc -O2 -fllvm -fforce-recomp euler12.hs; time ./euler12                                                                                      [9:40]
[1 of 1] Compiling Main             ( euler12.hs, euler12.o )
Linking euler12 ...
842161320
./euler12  9.43s user 0.13s system 99% cpu 9.602 total

สรุป: การไม่ทำอะไรเลยกับ ghc นั้นเป็นคอมไพเลอร์ที่ยอดเยี่ยม แต่โดยทั่วไป gcc จะสร้างโค้ดที่เร็วกว่า


22
ดีมาก! สำหรับการเปรียบเทียบในเครื่องของฉันวิธีการแก้ปัญหาของคุณ C ทำงานใน2.5 secondsขณะที่การปรับเปลี่ยนคล้ายกับรหัส Haskell (ย้ายไป Word32 เพิ่ม pragma INLINE) 4.8 secondsผลในการรันไทม์ของ บางทีสิ่งที่สามารถทำได้ (ดูเหมือนไม่ประหม่า) - ผล gcc นั้นน่าประทับใจอย่างแน่นอน
Thomas M. DuBuisson

1
ขอบคุณ! บางทีคำถามควรเป็นความเร็วของการรวบรวมผลลัพธ์โดยคอมไพเลอร์ต่าง ๆ มากกว่าภาษาจริง จากนั้นอีกครั้งการดึงคู่มือ Intel และการเพิ่มประสิทธิภาพด้วยมือจะยังคงชนะทันที (หากคุณมีความรู้และเวลา (มาก))
Raedwulf

56

ลองดูที่บล็อกนี้ ในช่วงปีที่ผ่านมาเขาทำปัญหาเล็กน้อยเกี่ยวกับ Project Euler ใน Haskell และ Python และโดยทั่วไปเขาพบว่าHaskellนั้นเร็วกว่ามาก ฉันคิดว่าระหว่างภาษาเหล่านั้นมันเกี่ยวข้องกับความคล่องแคล่วและสไตล์การเขียนโค้ดของคุณมากกว่า

เมื่อพูดถึงความเร็วของ Python คุณกำลังใช้งานผิดปกติ! ลองPyPyและสิ่งต่าง ๆ เช่นนี้คุณจะพบว่ามันเร็วกว่ามาก


32

การใช้งาน Haskell ของคุณอาจเพิ่มขึ้นอย่างมากโดยใช้ฟังก์ชั่นบางอย่างจากแพ็คเกจ Haskell ในกรณีนี้ฉันใช้เฉพาะช่วงเวลาซึ่งเพิ่งติดตั้งด้วย 'cabal install primes';)

import Data.Numbers.Primes
import Data.List

triangleNumbers = scanl1 (+) [1..]
nDivisors n = product $ map ((+1) . length) (group (primeFactors n))
answer = head $ filter ((> 500) . nDivisors) triangleNumbers

main :: IO ()
main = putStrLn $ "First triangle number to have over 500 divisors: " ++ (show answer)

การกำหนดเวลา:

โปรแกรมดั้งเดิมของคุณ:

PS> measure-command { bin\012_slow.exe }

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

ปรับปรุงการใช้งาน

PS> measure-command { bin\012.exe }

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

อย่างที่คุณเห็น, อันนี้ทำงานใน 38 มิลลิวินาทีบนเครื่องเดียวกับที่คุณวิ่งใน 16 วินาที :)

คำสั่งการรวบรวม:

ghc -O2 012.hs -o bin\012.exe
ghc -O2 012_slow.hs -o bin\012_slow.exe

5
ล่าสุดฉันตรวจสอบ Haskell "ช่วงเวลา" เป็นเพียงรายการใหญ่ของช่วงเวลาที่คำนวณล่วงหน้า - ไม่มีการคำนวณเพียงค้นหา ใช่แน่นอนว่ามันจะเร็วขึ้น แต่มันไม่ได้บอกอะไรคุณเกี่ยวกับความเร็วในการคำนวณของการได้รับช่วงเวลาใน Haskell
zxq9

21
@ zxq9 คุณสามารถชี้ให้ฉันที่ในแหล่งแพคเกจเฉพาะ ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/... ) รายชื่อของตัวเลขที่สำคัญที่มีอยู่?
เฟรเซอร์

4
ในขณะที่แหล่งที่มาแสดงให้เห็นถึงช่วงเวลาที่ไม่ได้คำนวณไว้ล่วงหน้าความเร็วนี้จะบ้ามากที่สุดไมล์เร็วกว่ารุ่น C ดังนั้นสิ่งที่ห่าเกิดขึ้น?
เซมิโคลอน

1
@semicolon ท่องจำ ในกรณีนี้ฉันคิดว่า Haskell จดจำช่วงเวลาทั้งหมดของรันไทม์ดังนั้นจึงไม่จำเป็นต้องคำนวณซ้ำทุกครั้ง
Hauleth

5
มันคือ 1,000 ตัวหารไม่ใช่ 500
Casper Færgemand

29

แค่เล่น ๆ. ต่อไปนี้คือการใช้งาน Haskell 'แบบดั้งเดิม' เพิ่มเติม:

import Control.Applicative
import Control.Monad
import Data.Either
import Math.NumberTheory.Powers.Squares

isInt :: RealFrac c => c -> Bool
isInt = (==) <$> id <*> fromInteger . round

intSqrt :: (Integral a) => a -> Int
--intSqrt = fromIntegral . floor . sqrt . fromIntegral
intSqrt = fromIntegral . integerSquareRoot'

factorize :: Int -> [Int]
factorize 1 = []
factorize n = first : factorize (quot n first)
  where first = (!! 0) $ [a | a <- [2..intSqrt n], rem n a == 0] ++ [n]

factorize2 :: Int -> [(Int,Int)]
factorize2 = foldl (\ls@((val,freq):xs) y -> if val == y then (val,freq+1):xs else (y,1):ls) [(0,0)] . factorize

numDivisors :: Int -> Int
numDivisors = foldl (\acc (_,y) -> acc * (y+1)) 1 <$> factorize2

nextTriangleNumber :: (Int,Int) -> (Int,Int)
nextTriangleNumber (n,acc) = (n+1,acc+n+1)

forward :: Int -> (Int, Int) -> Either (Int, Int) (Int, Int)
forward k val@(n,acc) = if numDivisors acc > k then Left val else Right (nextTriangleNumber val)

problem12 :: Int -> (Int, Int)
problem12 n = (!!0) . lefts . scanl (>>=) (forward n (1,1)) . repeat . forward $ n

main = do
  let (n,val) = problem12 1000
  print val

ใช้ghc -O3นี่ทำงานอย่างต่อเนื่องใน 0.55-0.58 วินาทีบนเครื่องของฉัน (1.73GHz Core i7)

ฟังก์ชัน factorCount ที่มีประสิทธิภาพมากขึ้นสำหรับเวอร์ชั่น C:

int factorCount (int n)
{
  int count = 1;
  int candidate,tmpCount;
  while (n % 2 == 0) {
    count++;
    n /= 2;
  }
    for (candidate = 3; candidate < n && candidate * candidate < n; candidate += 2)
    if (n % candidate == 0) {
      tmpCount = 1;
      do {
        tmpCount++;
        n /= candidate;
      } while (n % candidate == 0);
       count*=tmpCount;
      }
  if (n > 1)
    count *= 2;
  return count;
}

การเปลี่ยน longs เป็น ints ใน main โดยใช้gcc -O3 -lmสิ่งนี้จะทำงานอย่างต่อเนื่องใน 0.31-0.35 วินาที

ทั้งคู่สามารถทำงานได้เร็วขึ้นหากคุณใช้ประโยชน์จากข้อเท็จจริงที่ว่าเลขสามเหลี่ยมที่ n = n * (n + 1) / 2 และ n และ (n + 1) มีการแยกตัวประกอบสำคัญทั้งหมดดังนั้นจำนวนของปัจจัย ของแต่ละครึ่งสามารถคูณเพื่อหาจำนวนของปัจจัยทั้งหมด ดังต่อไปนี้:

int main ()
{
  int triangle = 0,count1,count2 = 1;
  do {
    count1 = count2;
    count2 = ++triangle % 2 == 0 ? factorCount(triangle+1) : factorCount((triangle+1)/2);
  } while (count1*count2 < 1001);
  printf ("%lld\n", ((long long)triangle)*(triangle+1)/2);
}

จะลดเวลาทำงานของรหัส c เป็น 0.17-0.19 วินาทีและสามารถจัดการการค้นหาที่ใหญ่กว่ามาก - มากกว่า 10,000 ปัจจัยใช้เวลาประมาณ 43 วินาทีในเครื่องของฉัน ฉันออกจากการเร่งความเร็ว Haskell ที่คล้ายกันให้กับผู้อ่านที่สนใจ


3
เพื่อการเปรียบเทียบ: เวอร์ชั่น c ต้นฉบับ: 9.1690, รุ่น thaumkid: 0.1060 การปรับปรุง 86x
thanos

ว้าว. Haskell ทำงานได้อย่างยอดเยี่ยมเมื่อคุณหลีกเลี่ยงประเภทที่อนุมาน
Piyush Katariya

จริงๆแล้วมันไม่ใช่การอนุมานที่ทำ ซึ่งจะช่วยให้คุณ A) การดีบักหรือหลีกเลี่ยงปัญหาการพิมพ์และปัญหาการเลือกอินสแตนซ์ของคลาส typeclass B) การดีบักและหลีกเลี่ยงปัญหาประเภท undecidable ที่มีนามสกุลภาษาที่ทันสมัยบางอย่าง นอกจากนี้ยังช่วยคุณในการทำให้โปรแกรมของคุณไม่รวมอยู่ในโปรแกรมดังนั้นคุณจึงไม่สามารถเพิ่มความพยายามในการพัฒนา
codehot

c เวอร์ชั่น 0.11 s บนหุบเขากะโหลกศีรษะของ Intel
codehot

13
คำถามที่ 1: ทำ erlang, python และ haskell หลวมความเร็วเนื่องจากการใช้จำนวนเต็มความยาวโดยพลการหรือไม่พวกเขาตราบใดที่ค่าน้อยกว่า MAXINT?

สิ่งนี้ไม่น่าเป็นไปได้ ฉันไม่สามารถพูดอะไรเกี่ยวกับ Erlang และ Haskell ได้ (อาจจะเกี่ยวกับ Haskell ด้านล่าง) แต่ฉันสามารถชี้จุดคอขวดอื่น ๆ จำนวนมากใน Python ทุกครั้งที่โปรแกรมพยายามดำเนินการกับค่าบางอย่างใน Python มันควรตรวจสอบว่าค่านั้นมาจากประเภทที่เหมาะสมหรือไม่และใช้เวลาสักครู่ factorCountฟังก์ชั่นของคุณเพียงแค่จัดสรรรายการที่มีrange (1, isquare + 1)หลาย ๆ ครั้งและการmallocจัดสรรหน่วยความจำแบบมีสไตล์นั้นช้ากว่าการวนซ้ำในช่วงที่มีตัวนับเหมือนกับที่คุณทำใน C โดยเฉพาะอย่างยิ่งสิ่งที่factorCount()เรียกว่าหลายครั้ง นอกจากนี้อย่าลืมว่ามีการตีความ Python และล่าม CPython ไม่ได้มุ่งเน้นที่การปรับให้เหมาะสม

แก้ไข : โอ้ดีฉันทราบว่าคุณใช้ Python 3 ดังนั้นrange()จะไม่ส่งคืนรายการ แต่เป็นตัวสร้าง ในกรณีนี้จุดของฉันเกี่ยวกับการจัดสรรรายการไม่ถูกต้องครึ่งหนึ่ง: ฟังก์ชั่นจัดสรรrangeวัตถุซึ่งไม่มีประสิทธิภาพ แต่ก็ไม่มีประสิทธิภาพเท่าการจัดสรรรายการที่มีรายการจำนวนมาก

คำถามที่ 2: ทำไมฮาเซลจึงช้าขนาดนี้? มีธงคอมไพเลอร์ที่ปิดเบรกหรือเป็นการติดตั้งของฉันหรือไม่? (อันหลังเป็นไปได้ค่อนข้างมากเพราะ Haskell เป็นหนังสือที่มีเจ็ดแมวน้ำให้ฉัน)

คุณกำลังใช้Hugsหรือไม่ Hugs เป็นล่ามที่ช้ามาก ถ้าคุณใช้มันบางทีคุณอาจจะได้เวลาที่ดีขึ้นกับGHC - แต่ฉันเป็นแค่ไฮโปติกซิงก์สิ่งที่คอมไพเลอร์ Haskell ที่ดีทำภายใต้ประทุนนั้นค่อนข้างน่าสนใจและเหนือความเข้าใจของฉัน :)

คำถามที่ 3: คุณสามารถให้คำแนะนำบางอย่างแก่ฉันถึงวิธีเพิ่มประสิทธิภาพการใช้งานเหล่านี้โดยไม่เปลี่ยนวิธีที่ฉันใช้พิจารณาปัจจัยหรือไม่ การเพิ่มประสิทธิภาพในทุกวิถีทาง: ดีกว่าเร็วขึ้นมากขึ้น "เป็นเจ้าของภาษา"

ฉันจะบอกว่าคุณกำลังเล่นเกมที่ไม่น่าสนุก ส่วนที่ดีที่สุดของการรู้ภาษาต่าง ๆ คือการใช้วิธีที่แตกต่างกันที่สุดเท่าที่จะเป็นไปได้ :) แต่ฉันพูดนอกเรื่องฉันก็ไม่มีคำแนะนำสำหรับประเด็นนี้ ขออภัยฉันหวังว่าจะมีคนช่วยคุณในกรณีนี้ :)

คำถามที่ 4: การใช้งานฟังก์ชั่นของฉันอนุญาต LCO หรือไม่และหลีกเลี่ยงการเพิ่มเฟรมที่ไม่จำเป็นลงใน call stack?

เท่าที่ฉันจำได้คุณเพียงแค่ต้องแน่ใจว่าการเรียกซ้ำของคุณเป็นคำสั่งสุดท้ายก่อนที่จะส่งคืนค่า กล่าวอีกนัยหนึ่งฟังก์ชั่นเช่นเดียวกับด้านล่างสามารถใช้การปรับให้เหมาะสมดังกล่าว:

def factorial(n, acc=1):
    if n > 1:
        acc = acc * n
        n = n - 1
        return factorial(n, acc)
    else:
        return acc

อย่างไรก็ตามคุณจะไม่ได้รับการปรับให้เหมาะสมหากฟังก์ชันของคุณเป็นเช่นด้านล่างเนื่องจากมีการดำเนินการ (การคูณ) หลังจากการเรียกซ้ำ

def factorial2(n):
    if n > 1:
        f = factorial2(n-1)
        return f*n
    else:
        return 1

ฉันแยกการดำเนินการในตัวแปรท้องถิ่นบางตัวเพื่อทำให้ชัดเจนว่าการดำเนินการใดจะถูกดำเนินการ อย่างไรก็ตามสิ่งที่พบบ่อยที่สุดคือการดูฟังก์ชั่นเหล่านี้ดังต่อไปนี้

def factorial(n, acc=1):
    if n > 1:
        return factorial(n-1, acc*n)
    else:
        return acc

def factorial2(n):
    if n > 1:
        return n*factorial(n-1)
    else:
        return 1

โปรดทราบว่ามันขึ้นอยู่กับคอมไพเลอร์ / ล่ามที่จะตัดสินใจว่ามันจะทำให้การเรียกซ้ำหาง ตัวอย่างเช่นล่าม Python ไม่สามารถทำได้ถ้าฉันจำได้ดี (ฉันใช้ Python ในตัวอย่างของฉันเท่านั้นเพราะไวยากรณ์ได้อย่างคล่องแคล่ว) อย่างไรก็ตามถ้าคุณพบสิ่งแปลก ๆ เช่นฟังก์ชั่นปัจจัยที่มีสองพารามิเตอร์ (และเป็นหนึ่งในพารามิเตอร์ที่มีชื่อเช่นacc, accumulatorฯลฯ ) ตอนนี้คุณรู้ว่าทำไมคนทำมัน :)


@Hyperboreus ขอบคุณ! นอกจากนี้ฉันอยากรู้เกี่ยวกับคำถามต่อไปของคุณ อย่างไรก็ตามฉันเตือนคุณว่าความรู้ของฉันมี จำกัด ดังนั้นฉันจึงไม่สามารถตอบคำถามทุกข้อของคุณได้ สำหรับการพยายามชดเชยมันฉันได้สร้างวิกิชุมชนของฉันเพื่อให้ผู้คนสามารถเติมเต็มได้ง่ายขึ้น
brandizzi

เกี่ยวกับการใช้ช่วง เมื่อฉันเปลี่ยนช่วงด้วยขณะที่ลูปด้วยการเพิ่ม (การเลียนแบบลูป for C) เวลาดำเนินการจะเพิ่มขึ้นเป็นสองเท่า ฉันคิดว่าเครื่องกำเนิดไฟฟ้าได้รับการปรับให้เหมาะสม
Hyperboreus

12

ด้วย Haskell คุณไม่จำเป็นต้องคิดซ้ำ ๆ อย่างชัดเจน

factorCount number = foldr factorCount' 0 [1..isquare] -
                     (fromEnum $ square == fromIntegral isquare)
    where
      square = sqrt $ fromIntegral number
      isquare = floor square
      factorCount' candidate
        | number `rem` candidate == 0 = (2 +)
        | otherwise = id

triangles :: [Int]
triangles = scanl1 (+) [1,2..]

main = print . head $ dropWhile ((< 1001) . factorCount) triangles

ในรหัสข้างต้นฉันได้แทนที่ recursions อย่างชัดเจนใน @Thomas 'คำตอบด้วยการดำเนินการรายการทั่วไป รหัสยังคงทำสิ่งเดียวกันโดยที่เราไม่ต้องกังวลเกี่ยวกับการเรียกซ้ำหาง มันทำงาน (~ 7.49s ) ประมาณ6%ช้ากว่ารุ่นใน @Thomas' คำตอบ (~ 7.04s ) ในเครื่องของฉันกับ GHC 7.6.2 ในขณะที่รุ่น C จาก @Raedwulf วิ่ง ~ 3.15s ดูเหมือนว่า GHC จะได้รับการปรับปรุงตลอดปี

PS ฉันรู้ว่ามันเป็นคำถามเก่าและฉันสะดุดมันจากการค้นหาของ Google (ฉันลืมสิ่งที่ฉันกำลังค้นหาตอนนี้ ... ) แค่ต้องการแสดงความคิดเห็นเกี่ยวกับคำถามเกี่ยวกับ LCO และแสดงความรู้สึกของฉันเกี่ยวกับ Haskell โดยทั่วไป ฉันต้องการที่จะแสดงความคิดเห็นในคำตอบด้านบน แต่ความคิดเห็นไม่อนุญาตให้บล็อกรหัส


9

ตัวเลขและคำอธิบายเพิ่มเติมสำหรับเวอร์ชั่น C เห็นได้ชัดว่าไม่มีใครทำมันในช่วงหลายปีที่ผ่านมา อย่าลืมโหวตคำตอบนี้เพื่อให้ทุกคนได้เห็นและเรียนรู้

ขั้นตอนที่หนึ่ง: เกณฑ์มาตรฐานของโปรแกรมของผู้เขียน

ข้อมูลจำเพาะของแล็ปท็อป:

  • CPU i3 M380 (931 MHz - โหมดประหยัดแบตเตอรี่สูงสุด)
  • หน่วยความจำ 4GB
  • Win7 64 บิต
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin กับ gcc 4.9.3
  • Python 2.7.10

คำสั่ง:

compiling on VS x64 command prompt > `for /f %f in ('dir /b *.c') do cl /O2 /Ot /Ox %f -o %f_x64_vs2012.exe`
compiling on cygwin with gcc x64   > `for f in ./*.c; do gcc -m64 -O3 $f -o ${f}_x64_gcc.exe ; done`
time (unix tools) using cygwin > `for f in ./*.exe; do  echo "----------"; echo $f ; time $f ; done`

.

----------
$ time python ./original.py

real    2m17.748s
user    2m15.783s
sys     0m0.093s
----------
$ time ./original_x86_vs2012.exe

real    0m8.377s
user    0m0.015s
sys     0m0.000s
----------
$ time ./original_x64_vs2012.exe

real    0m8.408s
user    0m0.000s
sys     0m0.015s
----------
$ time ./original_x64_gcc.exe

real    0m20.951s
user    0m20.732s
sys     0m0.030s

ชื่อไฟล์คือ: integertype_architecture_compiler.exe

  • จำนวนเต็มเหมือนกับโปรแกรมต้นฉบับสำหรับตอนนี้ (เพิ่มเติมในภายหลัง)
  • สถาปัตยกรรมเป็น x86 หรือ x64 ขึ้นอยู่กับการตั้งค่าคอมไพเลอร์
  • คอมไพเลอร์คือ gcc หรือ vs2012

ขั้นตอนที่สอง: ตรวจสอบปรับปรุงและสร้างมาตรฐานอีกครั้ง

VS นั้นเร็วกว่า gcc 250% คอมไพเลอร์ทั้งสองควรให้ความเร็วใกล้เคียงกัน เห็นได้ชัดว่ามีบางอย่างผิดปกติกับรหัสหรือตัวเลือกคอมไพเลอร์ มาตรวจสอบกัน!

จุดแรกที่น่าสนใจคือประเภทจำนวนเต็ม การแปลงอาจมีราคาแพงและความสม่ำเสมอเป็นสิ่งสำคัญสำหรับการสร้างและปรับแต่งโค้ดที่ดีขึ้น จำนวนเต็มทั้งหมดควรเป็นประเภทเดียวกัน

มันเป็นระเบียบผสมintและlongในขณะนี้ เราจะปรับปรุงมัน ประเภทใดที่จะใช้? ที่เร็วที่สุด. ต้องเทียบเคียงมาตรฐานพวกเขาทั้งหมด!

----------
$ time ./int_x86_vs2012.exe

real    0m8.440s
user    0m0.016s
sys     0m0.015s
----------
$ time ./int_x64_vs2012.exe

real    0m8.408s
user    0m0.016s
sys     0m0.015s
----------
$ time ./int32_x86_vs2012.exe

real    0m8.408s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int32_x64_vs2012.exe

real    0m8.362s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int64_x86_vs2012.exe

real    0m18.112s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int64_x64_vs2012.exe

real    0m18.611s
user    0m0.000s
sys     0m0.015s
----------
$ time ./long_x86_vs2012.exe

real    0m8.393s
user    0m0.015s
sys     0m0.000s
----------
$ time ./long_x64_vs2012.exe

real    0m8.440s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint32_x86_vs2012.exe

real    0m8.362s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint32_x64_vs2012.exe

real    0m8.393s
user    0m0.015s
sys     0m0.015s
----------
$ time ./uint64_x86_vs2012.exe

real    0m15.428s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint64_x64_vs2012.exe

real    0m15.725s
user    0m0.015s
sys     0m0.015s
----------
$ time ./int_x64_gcc.exe

real    0m8.531s
user    0m8.329s
sys     0m0.015s
----------
$ time ./int32_x64_gcc.exe

real    0m8.471s
user    0m8.345s
sys     0m0.000s
----------
$ time ./int64_x64_gcc.exe

real    0m20.264s
user    0m20.186s
sys     0m0.015s
----------
$ time ./long_x64_gcc.exe

real    0m20.935s
user    0m20.809s
sys     0m0.015s
----------
$ time ./uint32_x64_gcc.exe

real    0m8.393s
user    0m8.346s
sys     0m0.015s
----------
$ time ./uint64_x64_gcc.exe

real    0m16.973s
user    0m16.879s
sys     0m0.030s

ประเภทจำนวนเต็มเป็นint long int32_t uint32_t int64_tและuint64_tจาก#include <stdint.h>

มีจำนวนเต็มเป็นจำนวนเต็มชนิดใน C รวมทั้งมีการเซ็นชื่อ / ไม่ได้ลงชื่อเพื่อเล่นพร้อมกับตัวเลือกในการคอมไพล์เป็น x86 หรือ x64 (เพื่อไม่ให้สับสนกับขนาดจำนวนจริง) นี่เป็นเวอร์ชั่นที่รวบรวมและรัน ^^

ขั้นตอนที่สาม: ทำความเข้าใจกับตัวเลข

ข้อสรุปที่ชัดเจน:

  • จำนวนเต็ม 32 บิตจะเร็วกว่า ~ ~ ~ ~ ~ ~ ~ ~ 64% เทียบเท่า 64%
  • จำนวนเต็ม64 บิตที่ไม่ได้ลงชื่อนั้นเร็วกว่าการลงนาม 64 บิต 25% (น่าเสียดายที่ฉันไม่มีคำอธิบายสำหรับเรื่องนั้น)

เคล็ดลับคำถาม: "ขนาดของ int และ long ใน C คือเท่าใด"
คำตอบที่ถูกคือขนาดของ int และ long ใน C นั้นไม่ได้กำหนดไว้อย่างดี!

จากข้อกำหนด C:

int มีความยาวอย่างน้อย 32 บิต
เป็นอย่างน้อย int

จากหน้า man gcc (แฟล็ก -m32 และ -m64):

สภาพแวดล้อมแบบ 32 บิตตั้งค่า int ยาวและตัวชี้เป็น 32 บิตและสร้างรหัสที่ทำงานบนระบบ i386 ใด ๆ
สภาพแวดล้อมแบบ 64 บิตตั้งค่า int เป็น 32 บิตและแบบยาวและตัวชี้เป็น 64 บิตและสร้างรหัสสำหรับสถาปัตยกรรม x86-64 ของ AMD

จากเอกสาร MSDN (ช่วงชนิดข้อมูล) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 ไบต์, รู้ได้ว่า
ยาวเป็นลายเซ็น, 4 ไบต์, รู้ได้ว่ายาวแค่ไหนและยาวเป็น int

เพื่อสรุปสิ่งนี้: บทเรียนที่ได้รับ

  • จำนวนเต็ม 32 บิตเร็วกว่าจำนวนเต็ม 64 บิต

  • ประเภทจำนวนเต็มมาตรฐานไม่ได้กำหนดไว้อย่างดีใน C หรือ C ++ ซึ่งจะแตกต่างกันไปขึ้นอยู่กับคอมไพเลอร์และสถาปัตยกรรม เมื่อคุณต้องการความมั่นคงและการคาดการณ์ใช้ครอบครัวจำนวนเต็มจากuint32_t#include <stdint.h>

  • แก้ไขปัญหาความเร็วแล้ว ภาษาอื่น ๆ ทั้งหมดกลับมาเป็นร้อย ๆ เปอร์เซ็นต์แล้ว C&C ++ จะชนะอีกครั้ง! พวกเขาทำเสมอ การปรับปรุงครั้งต่อไปจะเป็นการมัลติเธรดโดยใช้ OpenMP: D


ด้วยความอยากรู้อยากเห็นคอมไพเลอร์ Intel จะทำอย่างไร โดยปกติแล้วพวกเขาจะใช้รหัสตัวเลขได้ดีที่สุด
kirbyfan64sos

คุณจะพบการอ้างอิงที่ระบุว่าข้อมูลจำเพาะ C รับประกันว่า "int อย่างน้อย 32 บิต"? สิ่งเดียวที่รับประกันได้ว่าฉันรู้คือขนาดขั้นต่ำINT_MINและINT_MAX(-32767 และ 32767 ซึ่งกำหนดข้อกำหนดที่intมีอย่างน้อย 16 บิต) longจะต้องมีขนาดใหญ่อย่างน้อยหนึ่งตัวintและค่าเฉลี่ยของข้อกำหนดช่วงlongคืออย่างน้อย 32 บิต
ShadowRanger

คุณดูเหมือนจะถูกต้อง stackoverflow.com/questions/1231147/is-int-in-c-always-32-bit
user5994461

8

ดูการใช้ Erlang ของคุณ ระยะเวลารวมถึงการเริ่มต้นของเครื่องเสมือนทั้งหมดเรียกใช้โปรแกรมของคุณและหยุดเครื่องเสมือน ค่อนข้างแน่ใจว่าการตั้งค่าและหยุดทำงาน erlang vm ต้องใช้เวลาพอสมควร

หากกำหนดเวลาเสร็จสิ้นภายในเครื่องเสมือน erlang ตัวเองผลลัพธ์จะแตกต่างกันในกรณีนั้นเราจะมีเวลาจริงสำหรับโปรแกรมที่มีปัญหาเท่านั้น มิฉะนั้นฉันเชื่อว่าเวลาทั้งหมดที่ดำเนินการโดยกระบวนการเริ่มต้นและการโหลดของ Erlang Vm บวกกับการหยุดชะงัก (ตามที่คุณใส่ไว้ในโปรแกรมของคุณ) รวมอยู่ในเวลาทั้งหมดซึ่งเป็นวิธีที่คุณใช้ โปรแกรมกำลังแสดงผล พิจารณาใช้ระยะเวลา Erlang timer:tc/1 or timer:tc/2 or timer:tc/3ตัวเองที่เราใช้เมื่อเราต้องการเวลาโปรแกรมของเราที่อยู่ในเครื่องเสมือนตัวเอง ด้วยวิธีนี้ผลลัพธ์จาก erlang จะไม่รวมเวลาที่ใช้ในการเริ่มและหยุด / ฆ่า / หยุดเครื่องเสมือน นั่นคือเหตุผลของฉันที่นั่นคิดเกี่ยวกับมันแล้วลองทำเครื่องหมายมาตรฐานของคุณอีกครั้ง

ฉันขอแนะนำให้เราลองใช้เวลาโปรแกรม (สำหรับภาษาที่มี runtime) ภายใน runtime ของภาษาเหล่านั้นเพื่อให้ได้ค่าที่แม่นยำ ตัวอย่างเช่น C ไม่มีค่าใช้จ่ายในการเริ่มต้นและปิดระบบรันไทม์เช่นเดียวกับ Erlang, Python และ Haskell (แน่ใจ 98% สำหรับสิ่งนี้ - ฉันยืนการแก้ไข) ดังนั้น (ตามเหตุผลนี้) ฉันสรุปด้วยการบอกว่าเกณฑ์มาตรฐานนี้ไม่แม่นยำ / ยุติธรรมเพียงพอสำหรับภาษาที่ใช้งานบนระบบรันไทม์ ให้ทำอีกครั้งกับการเปลี่ยนแปลงเหล่านี้

แก้ไข: นอกจากแม้ว่าภาษาทั้งหมดจะมีระบบรันไทม์ค่าใช้จ่ายในการเริ่มต้นแต่ละภาษาและหยุดการทำงานจะแตกต่างกัน ดังนั้นฉันขอแนะนำให้เราเวลาจากภายในระบบรันไทม์ (สำหรับภาษาที่ใช้) Erlang VM เป็นที่รู้จักกันว่ามีค่าใช้จ่ายจำนวนมากเมื่อเริ่มต้นขึ้น!


ฉันลืมที่จะพูดถึงมันในโพสต์ของฉัน แต่ฉันวัดเวลาที่ใช้ในการเริ่มต้นระบบ (erl -noshell -s erlang halt) - ประมาณ 0.1 วินาทีในเครื่องของฉัน นี้มีขนาดเล็กพอเมื่อเทียบกับเวลารันของโปรแกรม (ประมาณ 10 วินาที) ที่ไม่คุ้มกับการเล่นโวหาร
RichardC

บนเครื่องของคุณ! เราไม่ทราบว่าคุณกำลังทำงานบนเซิร์ฟเวอร์ไฟไหม้! เนื่องจากเวลาเป็นตัวแปรที่แปรผันตามข้อกำหนดของเครื่องจึงควรคำนึงถึง .... การพูดคลุมเครือ?
Muzaaya Joshua

2
@RichardC ไม่มีที่ไหนพูดถึงว่า Erlang เร็วกว่า :) มันมีเป้าหมายที่แตกต่างไม่ใช่ความเร็ว!
ข้อยกเว้น

7

คำถามที่ 1: Erlang, Python และ Haskell สูญเสียความเร็วเนื่องจากการใช้จำนวนเต็มความยาวตามอำเภอใจหรือไม่ตราบเท่าที่ค่าน้อยกว่า MAXINT?

คำถามหนึ่งสามารถตอบในเชิงลบสำหรับ Erlang คำถามสุดท้ายนั้นตอบโดยใช้ Erlang อย่างเหมาะสมเช่นเดียวกับใน:

http://bredsaal.dk/learning-erlang-using-projecteuler-net

เนื่องจากมันเร็วกว่าตัวอย่าง C แรกของคุณฉันจะเดาว่ามีปัญหามากมายตามที่คนอื่น ๆ ได้กล่าวถึงในรายละเอียดแล้ว

โมดูล Erlang นี้ทำงานบนเน็ตบุ๊กราคาถูกในเวลาประมาณ 5 วินาที ... มันใช้โมเดลเธรดเครือข่ายใน erlang และดังที่แสดงให้เห็นถึงวิธีการใช้ประโยชน์จากโมเดลเหตุการณ์ มันสามารถกระจายไปหลายโหนด และมันก็รวดเร็ว ไม่ใช่รหัสของฉัน

-module(p12dist).  
-author("Jannich Brendle, jannich@bredsaal.dk, http://blog.bredsaal.dk").  
-compile(export_all).

server() ->  
  server(1).

server(Number) ->  
  receive {getwork, Worker_PID} -> Worker_PID ! {work,Number,Number+100},  
  server(Number+101);  
  {result,T} -> io:format("The result is: \~w.\~n", [T]);  
  _ -> server(Number)  
  end.

worker(Server_PID) ->  
  Server_PID ! {getwork, self()},  
  receive {work,Start,End} -> solve(Start,End,Server_PID)  
  end,  
  worker(Server_PID).

start() ->  
  Server_PID = spawn(p12dist, server, []),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]).

solve(N,End,_) when N =:= End -> no_solution;

solve(N,End,Server_PID) ->  
  T=round(N*(N+1)/2),
  case (divisor(T,round(math:sqrt(T))) > 500) of  
    true ->  
      Server_PID ! {result,T};  
    false ->  
      solve(N+1,End,Server_PID)  
  end.

divisors(N) ->  
  divisor(N,round(math:sqrt(N))).

divisor(_,0) -> 1;  
divisor(N,I) ->  
  case (N rem I) =:= 0 of  
  true ->  
    2+divisor(N,I-1);  
  false ->  
    divisor(N,I-1)  
  end.

การทดสอบด้านล่างเกิดขึ้นกับ: Intel (R) Atom (TM) CPU N270 @ 1.60GHz

~$ time erl -noshell -s p12dist start

The result is: 76576500.

^C

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a

real    0m5.510s
user    0m5.836s
sys 0m0.152s

การเพิ่มค่าเป็น 1,000 ดังต่อไปนี้ไม่ได้ผลลัพธ์ที่ถูกต้อง ด้วย 500 การทดสอบข้างต้นใหม่ล่าสุด: IntelCore2 CPU 6600 @ 2.40GHz มาใน 0m2.370s จริง
Mark Washeim

ผลลัพธ์ของคุณ: 76576500 ทุกคน: 842161320 มีบางอย่างที่ผิดกับผลลัพธ์ของคุณ
davidDavidson

เนื่องจากฉันเป็นดงปัญหาออยเลอร์อื่น ๆ ฉันเพิ่งตรวจสอบผลลัพธ์ของฉัน คำตอบสำหรับprojecteuler.net/problem=12คือ 76576500 ไม่มีคำถามเกี่ยวกับมัน ฉันรู้ว่ามันดูแปลก แต่ฉันเพิ่งตรวจสอบ
Mark Washeim

สำหรับการเปรียบเทียบฉันได้ 9.03 กับรุ่น c ต้นฉบับในขณะที่ใช้ Erlang 19 พร้อมรหัสของ Mark ฉันได้รับ 5.406 เร็วขึ้น 167.0366%
thanos

5

C ++ 11, <20ms สำหรับฉัน - เรียกใช้ที่นี่

ฉันเข้าใจว่าคุณต้องการเคล็ดลับที่จะช่วยพัฒนาความรู้เฉพาะด้านภาษาของคุณ แต่เนื่องจากสิ่งเหล่านี้ได้รับการคุ้มครองอย่างดีที่นี่ฉันคิดว่าฉันจะเพิ่มบริบทให้กับผู้ที่อาจได้ดูข้อคิดเห็นทางคณิตศาสตร์ในคำถามของคุณ ฯลฯ และสงสัยว่าทำไม รหัสช้าลงมาก

คำตอบนี้ส่วนใหญ่จะให้บริบทเพื่อหวังว่าจะช่วยให้ผู้คนประเมินรหัสในคำถามของคุณ / คำตอบอื่น ๆ ได้ง่ายขึ้น

รหัสนี้ใช้การเพิ่มประสิทธิภาพเพียงเล็กน้อย (น่าเกลียด) ซึ่งไม่เกี่ยวข้องกับภาษาที่ใช้โดยยึดตาม:

  1. หมายเลข traingle ทุกตัวมีรูปแบบ n (n + 1) / 2
  2. n และ n + 1 คือ coprime
  3. จำนวนตัวหารเป็นฟังก์ชันการคูณ

#include <iostream>
#include <cmath>
#include <tuple>
#include <chrono>

using namespace std;

// Calculates the divisors of an integer by determining its prime factorisation.

int get_divisors(long long n)
{
    int divisors_count = 1;

    for(long long i = 2;
        i <= sqrt(n);
        /* empty */)
    {
        int divisions = 0;
        while(n % i == 0)
        {
            n /= i;
            divisions++;
        }

        divisors_count *= (divisions + 1);

        //here, we try to iterate more efficiently by skipping
        //obvious non-primes like 4, 6, etc
        if(i == 2)
            i++;
        else
            i += 2;
    }

    if(n != 1) //n is a prime
        return divisors_count * 2;
    else
        return divisors_count;
}

long long euler12()
{
    //n and n + 1
    long long n, n_p_1;

    n = 1; n_p_1 = 2;

    // divisors_x will store either the divisors of x or x/2
    // (the later iff x is divisible by two)
    long long divisors_n = 1;
    long long divisors_n_p_1 = 2;

    for(;;)
    {
        /* This loop has been unwound, so two iterations are completed at a time
         * n and n + 1 have no prime factors in common and therefore we can
         * calculate their divisors separately
         */

        long long total_divisors;                 //the divisors of the triangle number
                                                  // n(n+1)/2

        //the first (unwound) iteration

        divisors_n_p_1 = get_divisors(n_p_1 / 2); //here n+1 is even and we

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

        //move n and n+1 forward
        n = n_p_1;
        n_p_1 = n + 1;

        //fix the divisors
        divisors_n = divisors_n_p_1;
        divisors_n_p_1 = get_divisors(n_p_1);   //n_p_1 is now odd!

        //now the second (unwound) iteration

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

        //move n and n+1 forward
        n = n_p_1;
        n_p_1 = n + 1;

        //fix the divisors
        divisors_n = divisors_n_p_1;
        divisors_n_p_1 = get_divisors(n_p_1 / 2);   //n_p_1 is now even!
    }

    return (n * n_p_1) / 2;
}

int main()
{
    for(int i = 0; i < 1000; i++)
    {
        using namespace std::chrono;
        auto start = high_resolution_clock::now();
        auto result = euler12();
        auto end = high_resolution_clock::now();

        double time_elapsed = duration_cast<milliseconds>(end - start).count();

        cout << result << " " << time_elapsed << '\n';
    }
    return 0;
}

ที่ใช้เวลาเฉลี่ยประมาณ 19ms สำหรับเดสก์ท็อปของฉันและ 80ms สำหรับแล็ปท็อปของฉันห่างไกลจากรหัสอื่น ๆ ส่วนใหญ่ที่ฉันเคยเห็นที่นี่ และไม่ต้องสงสัยเลยว่ามีการเพิ่มประสิทธิภาพหลายอย่างให้เลือก


7
นี่ไม่ใช่สิ่งที่ผู้ถามร้องขออย่างชัดเจน "ฉันพยายามใช้อัลกอริทึมเดียวกันกับที่ใกล้เคียงที่สุดในสี่ภาษา" หากต้องการพูดถึงความคิดเห็นเกี่ยวกับหนึ่งในคำตอบที่ถูกลบซึ่งคล้ายกับของคุณ "ค่อนข้างชัดเจนว่าคุณสามารถรับความเร็วได้เร็วขึ้นด้วยอัลกอริทึมที่ดีกว่าโดยไม่คำนึงถึงภาษา"
Thomas M. DuBuisson

2
@ ThomasM.DuBuisson นั่นคือสิ่งที่ฉันได้รับ คำถาม \ คำตอบที่บ่งบอกอย่างชัดเจนว่าอัลกอริทึมการเพิ่มความเร็วอย่างมีนัยสำคัญ (และแน่นอนว่า OP ไม่ได้ขอให้พวกเขา) แต่ไม่มีตัวอย่างที่ชัดเจน ฉันคิดว่าคำตอบนี้ - ซึ่งไม่ใช่รหัสที่ได้รับการปรับให้เหมาะสมที่สุด - ให้บริบทที่เป็นประโยชน์เล็กน้อยสำหรับทุกคนเช่นตัวฉันเองซึ่งสงสัยว่ารหัส OP ของช้าหรือเร็วเพียงใด
user3125280

gcc สามารถคำนวณรูปแบบล่วงหน้าได้มาก int a = 0; สำหรับ (int i = 0; i <10,000000; ++ i) {a + = i;} จะถูกคำนวณ ณ เวลาที่คอมไพล์ดังนั้นให้ใช้เวลา <1ms ที่รันไทม์ นับด้วย
Arthur

@ โทมัส: ฉันต้องเห็นด้วยกับผู้ใช้ 3125280 - ภาษาควรจะเปรียบเทียบว่าพวกเขาชอบทำอะไรที่ชาญฉลาดแทนที่จะเปรียบเทียบว่าพวกเขาล้มเหลวในการเอาชนะภาษาโปรแกรมจริงเมื่อทำอะไรที่เป็นใบ้ อัลกอริธึมสมาร์ทมักจะใส่ใจเกี่ยวกับประสิทธิภาพของกล้องจุลทรรศน์น้อยกว่าเกี่ยวกับความยืดหยุ่นความสามารถในการโยงสิ่งต่าง ๆ (รวมเข้าด้วยกัน) และโครงสร้างพื้นฐาน ประเด็นก็คือไม่มากว่าใครจะได้ 20 ms หรือ 50 ms ก็ไม่ได้ 8 วินาทีหรือ 8 นาที
DarthGizka

5

ลองใช้ GO:

package main

import "fmt"
import "math"

func main() {
    var n, m, c int
    for i := 1; ; i++ {
        n, m, c = i * (i + 1) / 2, int(math.Sqrt(float64(n))), 0
        for f := 1; f < m; f++ {
            if n % f == 0 { c++ }
    }
    c *= 2
    if m * m == n { c ++ }
    if c > 1001 {
        fmt.Println(n)
        break
        }
    }
}

ฉันเข้าใจ:

รุ่น c ต้นฉบับ: 9.1690 100%
ไป: 8.2520 111%

แต่ใช้:

package main

import (
    "math"
    "fmt"
 )

// Sieve of Eratosthenes
func PrimesBelow(limit int) []int {
    switch {
        case limit < 2:
            return []int{}
        case limit == 2:
            return []int{2}
    }
    sievebound := (limit - 1) / 2
    sieve := make([]bool, sievebound+1)
    crosslimit := int(math.Sqrt(float64(limit))-1) / 2
    for i := 1; i <= crosslimit; i++ {
        if !sieve[i] {
            for j := 2 * i * (i + 1); j <= sievebound; j += 2*i + 1 {
                sieve[j] = true
            }
        }
    }
    plimit := int(1.3*float64(limit)) / int(math.Log(float64(limit)))
    primes := make([]int, plimit)
    p := 1
    primes[0] = 2
    for i := 1; i <= sievebound; i++ {
        if !sieve[i] {
            primes[p] = 2*i + 1
            p++
            if p >= plimit {
                break
            }
        }
    }
    last := len(primes) - 1
    for i := last; i > 0; i-- {
        if primes[i] != 0 {
            break
        }
        last = i
    }
    return primes[0:last]
}



func main() {
    fmt.Println(p12())
}
// Requires PrimesBelow from utils.go
func p12() int {
    n, dn, cnt := 3, 2, 0
    primearray := PrimesBelow(1000000)
    for cnt <= 1001 {
        n++
        n1 := n
        if n1%2 == 0 {
            n1 /= 2
        }
        dn1 := 1
        for i := 0; i < len(primearray); i++ {
            if primearray[i]*primearray[i] > n1 {
                dn1 *= 2
                break
            }
            exponent := 1
            for n1%primearray[i] == 0 {
                exponent++
                n1 /= primearray[i]
            }
            if exponent > 1 {
                dn1 *= exponent
            }
            if n1 == 1 {
                break
            }
        }
        cnt = dn * dn1
        dn = dn1
    }
    return n * (n - 1) / 2
}

ฉันเข้าใจ:

รุ่น c ต้นฉบับ: 9.1690 100%
รุ่น c ของ thaumkid: 0.1060 8650%
รุ่นที่ใช้งานครั้งแรก: 8.2520 111%
รุ่นที่สองที่ไป: 0.0230 39865%

ฉันยังลอง Python3.6 และ pypy3.3-5.5-alpha:

รุ่น c ต้นฉบับ: 8.629 100%
รุ่น c ของ thaumkid: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

แล้วด้วยรหัสต่อไปนี้ฉันได้รับ:

รุ่น c ต้นฉบับ: 8.629 100%
รุ่น c ของ thaumkid: 0.109 8650%
Python3.6: 1.489 580%
pypy3.3-5.5-alpha: 0.582 1483%

def D(N):
    if N == 1: return 1
    sqrtN = int(N ** 0.5)
    nf = 1
    for d in range(2, sqrtN + 1):
        if N % d == 0:
            nf = nf + 1
    return 2 * nf - (1 if sqrtN**2 == N else 0)

L = 1000
Dt, n = 0, 0

while Dt <= L:
    t = n * (n + 1) // 2
    Dt = D(n/2)*D(n+1) if n%2 == 0 else D(n)*D((n+1)/2)
    n = n + 1

print (t)

1

เปลี่ยนแปลง: case (divisor(T,round(math:sqrt(T))) > 500) of

ถึง: case (divisor(T,round(math:sqrt(T))) > 1000) of

สิ่งนี้จะสร้างคำตอบที่ถูกต้องสำหรับตัวอย่างหลายขั้นตอนของ Erlang


2
สิ่งนี้ตั้งใจให้เป็นความคิดเห็นในคำตอบนี้หรือไม่? เพราะมันไม่ชัดเจนและนี่ไม่ใช่คำตอบของตัวเอง
ShadowRanger

1

ฉันสันนิษฐานว่าจำนวนปัจจัยมีขนาดใหญ่ก็ต่อเมื่อตัวเลขที่เกี่ยวข้องมีปัจจัยเล็ก ๆ มากมาย ดังนั้นฉันจึงใช้อัลกอริทึมที่ยอดเยี่ยมของ thaumkid แต่ก่อนอื่นให้ใช้การประมาณกับจำนวนตัวคูณที่ไม่เล็กเกินไป มันค่อนข้างง่าย: ตรวจสอบปัจจัยสำคัญสูงสุดถึง 29 จากนั้นตรวจสอบจำนวนที่เหลือและคำนวณขอบเขตบนสำหรับปัจจัยต่างๆ ใช้สิ่งนี้เพื่อคำนวณขอบเขตบนสำหรับจำนวนปัจจัยและถ้าจำนวนนั้นสูงพอให้คำนวณจำนวนปัจจัยที่แน่นอน

รหัสด้านล่างไม่จำเป็นต้องมีข้อสันนิษฐานนี้เพื่อความถูกต้อง แต่จะรวดเร็ว ดูเหมือนว่าจะทำงาน มีเพียงประมาณหนึ่งใน 100,000 เท่านั้นที่ให้ค่าประมาณที่สูงพอที่จะต้องใช้การตรวจสอบเต็มรูปแบบ

นี่คือรหัส:

// Return at least the number of factors of n.
static uint64_t approxfactorcount (uint64_t n)
{
    uint64_t count = 1, add;

#define CHECK(d)                            \
    do {                                    \
        if (n % d == 0) {                   \
            add = count;                    \
            do { n /= d; count += add; }    \
            while (n % d == 0);             \
        }                                   \
    } while (0)

    CHECK ( 2); CHECK ( 3); CHECK ( 5); CHECK ( 7); CHECK (11); CHECK (13);
    CHECK (17); CHECK (19); CHECK (23); CHECK (29);
    if (n == 1) return count;
    if (n < 1ull * 31 * 31) return count * 2;
    if (n < 1ull * 31 * 31 * 37) return count * 4;
    if (n < 1ull * 31 * 31 * 37 * 37) return count * 8;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41) return count * 16;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43) return count * 32;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47) return count * 64;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53) return count * 128;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59) return count * 256;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61) return count * 512;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67) return count * 1024;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71) return count * 2048;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71 * 73) return count * 4096;
    return count * 1000000;
}

// Return the number of factors of n.
static uint64_t factorcount (uint64_t n)
{
    uint64_t count = 1, add;

    CHECK (2); CHECK (3);

    uint64_t d = 5, inc = 2;
    for (; d*d <= n; d += inc, inc = (6 - inc))
        CHECK (d);

    if (n > 1) count *= 2; // n must be a prime number
    return count;
}

// Prints triangular numbers with record numbers of factors.
static void printrecordnumbers (uint64_t limit)
{
    uint64_t record = 30000;

    uint64_t count1, factor1;
    uint64_t count2 = 1, factor2 = 1;

    for (uint64_t n = 1; n <= limit; ++n)
    {
        factor1 = factor2;
        count1 = count2;

        factor2 = n + 1; if (factor2 % 2 == 0) factor2 /= 2;
        count2 = approxfactorcount (factor2);

        if (count1 * count2 > record)
        {
            uint64_t factors = factorcount (factor1) * factorcount (factor2);
            if (factors > record)
            {
                printf ("%lluth triangular number = %llu has %llu factors\n", n, factor1 * factor2, factors);
                record = factors;
            }
        }
    }
}

พบสามเหลี่ยม 14,753,024 ตัวที่มี 13824 ปัจจัยในเวลาประมาณ 0.7 วินาที, รูปสามเหลี่ยม 879,207,615th ที่มี 61,440 ปัจจัยใน 34 วินาที, สามเหลี่ยมสามเหลี่ยม 12,524,486,975 ใน 10 นาที 5 วินาทีและ 26,467,792,064 21 นาที 25 วินาที (2.4GHz Core2 Duo) ดังนั้นรหัสนี้ใช้เวลาประมวลผลเพียง 116 รอบต่อหมายเลขโดยเฉลี่ย ตัวเลขสามเหลี่ยมตัวสุดท้ายมีขนาดใหญ่กว่า 2 ^ 68 ดังนั้น


0

ฉันแก้ไขเวอร์ชัน "Jannich Brendle" เป็น 1,000 แทน 500 และแสดงรายการผลลัพธ์ของ euler12.bin, euler12.erl, p12dist.erl รหัส erl ทั้งสองใช้ '+ native' เพื่อคอมไพล์

zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s p12dist start
The result is: 842161320.

real    0m3.879s
user    0m14.553s
sys     0m0.314s
zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s euler12 solve
842161320

real    0m10.125s
user    0m10.078s
sys     0m0.046s
zhengs-MacBook-Pro:workspace zhengzhibin$ time ./euler12.bin 
842161320

real    0m5.370s
user    0m5.328s
sys     0m0.004s
zhengs-MacBook-Pro:workspace zhengzhibin$

0
#include <stdio.h>
#include <math.h>

int factorCount (long n)
{
    double square = sqrt (n);
    int isquare = (int) square+1;
    long candidate = 2;
    int count = 1;
    while(candidate <= isquare && candidate<=n){
        int c = 1;
        while (n % candidate == 0) {
           c++;
           n /= candidate;
        }
        count *= c;
        candidate++;
    }
    return count;
}

int main ()
{
    long triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index ++;
        triangle += index;
    }
    printf ("%ld\n", triangle);
}

gcc -lm - euler.c รวดเร็ว

เวลา. /a.out

ผู้ใช้ 2.79s ระบบ 0.00s 99% cpu ทั้งหมด 2.794

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.