Permalänk
Datavetare

Progblemet #5

Har sett en del frågor från personer som haft någon programmeringsuppgift som involverar primtal, så varför inte köra ett progblemet kring detta för att visa hur det ser ut i lite olika språk.

Primtal är ett positivt heltal som bara är jämt delbart med 1 och sig sjäv. De första tio primtalen är

2, 3, 5, 7, 11, 13, 17, 19, 23 och 29

En av de mer effektiva algoritmer för att beräkna primtal upp till ett par miljoner kallas sieve of eratosthenes. Denna algoritm kan implementeras väldigt effektivt i t.ex. C eller C++ genom att skapa en bit-array där varje bit representerar udda nummer (jämna nummer kan aldrig vara primtal då de är delbara med 2). En nackdel med denna algoritm är att den beräknar alla primtal upp till en given gräns.

Problemet som ska lösas denna gång är att beräkna det miljonte primtalet. Värdet på detta är 15,485,863.

Här kommer en lösning i ett språk jag verkligen börjar gilla skarpt, Go.

package main import "fmt" import "math" func isPrime(n uint, primes []uint) bool { limit := uint(math.Sqrt(float64(n))) for i := 0; primes[i] <= limit; i++ { if n % primes[i] == 0 { return false } } return true } func producePrimes(ch chan uint) { primes := []uint{3} ch<- 2 ch<- 3 for n := uint(5); ; n += 2 { if isPrime(n, primes) { ch<- n primes = append(primes, n) } } } func main() { primes := make(chan uint) defer close(primes) go producePrimes(primes) for i := 1; i < 1000000; i++ { <-primes } fmt.Println(<-primes) }

Första raden i "main" skapar en kanal. En kanal i Go är en starkt typad enkelriktad kommunikationskanal, i detta fall är det en kanal av positiva heltal, uint.

Nästa är rent tekniskt onödig i detta program då alla resurser städas upp när main avslutas, men tar med den för att visa en väldigt trevlig finess i Go. defer är ett nyckelord som säger att den "closure" som följer måste köras när man avslutar nuvarande funktion, oavsett hur man avslutar funktionen. I detta fall stänger jag kommunikationskanalen, d.v.s close(primes) kommer inte köras vid rad 2 utan precis innan man avslutar main.

Sedan startas en asynkron "closure" m.h.a. nyckelordet go. I detta fall är det funktionen producePrime jag startar asynkront. Den funktion kan köras på en annan CPU-kärna på ett system som har flera CPU-kärnor.

<-primes läser ett värde som den andra änden, producePrime i detta fall, skriver. Resultatet är en värde av typen som kanalen har, uint i detta fall. De första 999,999 primtalen slängs bara bort och det 1,000,000:e skrivs ut.

Produce prime skapar en s.k. "slice" som innehåller alla udda primtal vi beräkat. En "slice" är väldigt lik en "array" i Python eller "vector<>" i C++, den kan alltså växa allt eftersom man lägger till element i slutet av den.

Första produceras primtalen 2 och 3. 5 är det första tal som testas om det är ett primtal och sedan testas vart annat tal efter det. Notera att producePrime inte kommer äta 100% CPU då kanalen jag skapat inte innehåller en buffer och således inte kan köa tal. Den som skriver till kanalen kommer i detta fall blocka om det inte finns någon som väntar på att läsa i andra änden, men det är möjligt att skapa kanaler som kan hålla ett eller flera tal i en intern buffer.

Detta fungerar alltså väldligt mycket som "lazy evaluation" som många funktionella språk har, d.v.s. ett värde beräknas inte innan man verkligen behöver det. Men man får även fördelen av att man potentiellt kan använda en separat CPU-tråd för beräkningen.

Dold text

Att göra denna beräkning tog 2.5s på en 2760QM (2.4GHz Sandy Bridge) körandes Ubuntu 12.10.

Edit: och för att förtydliga idén kring "progblemet" trådarna. Tanken är att de som har kunskapen och orken ska posta sina egna lösningar till detta problem. Tanken är att det kommer visa interesserade personer hur man löser ett relativt enkelt problem på lite olika sätt och framförallt hur dessa lösningar kan gestalta sig i lite olika programmeringspråk!

Det är naturligtvis helt OK att posta flera lösningar i språk man själv eller andra redan postat en lösning i. Det finns ju ofta långt mer än ett sätt att lösa ett givet problem.

Att jag lagt in exikveringstiderna är att många verkar gilla att jämföra hastigheten på sina lösningar med andra

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare

Clojure

Och här är en logiskt sett nära identisk lösning i Clojure. Denna lösning använder sig just av "lazy evaluation", men kan inte använda en extra CPU-kärna som Go.

(defn prime? [n pr-lst] "Returns true if n is a prime number" (let [limit (int (Math/sqrt n))] (every? #(< 0 (rem n %)) (take-while #(>= limit %) pr-lst)))) (defn primes "Returns a lazy sequence of all prime numbers" ([] (cons 2 (primes 3 []))) ([n ps] (if (prime? n ps) (let [new-ps (conj ps n)] (lazy-seq (cons n (primes (+ n 2) new-ps)))) (recur (+ n 2) ps)))) (println (first (drop 999999 (primes))))

I detta fall skapar funktionen primes en oändlig lista av alla primtal. De första 999,999 primtalen droppas med drop och det första talet i den kvarvarande listan skrivs ut.

Funktionen primes skapar en "lat" sekvens av tal m.h.a lazy-seq och det är en svansrekursiv funktion som anropar sig själv genom nykelordet recur. primes är även en polymorfisk funktion som kan anropas med noll argument eller två argument, nästa tal att testa som primtal och en lista av de primtal man hittat så här långt. lazy-seq ser till att inte köra sin "kropp" oftar än att så många nummer man frågat efter kommer att produceras, så denna funktion kommer inte dra 100% CPU hela tiden och äta upp allt minne. Well, i alla fall inte så länge man försöker t.e.x skriva hela listan som skapas av primes

Dold text

Denna lösning tog 48.6s att köra på laptop:en med 2760QM och 64-bitars Ubuntu 12.10.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare

Var ett tag sedan jag gjorde något i C#, så här är en lite annorlunda variant av att räkna primtal.

1. Skapar en enumerator som generarar en oändligt serie av heltal med början från 2.

2. GenerateAndFilterPrime plockar sedan ut första talet i serien (2 som är ett primtal), yield:ar primtalet och skapar sedan en oändligt ström där alla faktorer av primtalet är bortfilterat.

3. Primes retunerar en oändligt ström av primtal och varje nytt primtal läggs till som ett filter till strömmen m.h.a. GenerateAndFilterPrime.

Detta är ungefär som man skulle kunna implementera Sieve of Eratosthenes i ett funktionellt språk. Nackdelen med denna lösning är att den är extremt långsam då den kommer allokera och accessa MASSOR med små minnesareor då varje primtal skapar en closure. Andra nackdelen är att för eller senare spräger man stacken p.g.a. hur C# kompilatorn skapar iteratorer när man använder yield.

Så har bara testat att genera primtal nummer 100.000 då man får slut på stack innan man når 1.000.000.

using System; using System.Collections.Generic; using System.Linq; namespace Prime { class MainClass { public static IEnumerator<int> NaturalNumbers() { int n = 2; // Generate all positive numbers starting from 2 while (true) { yield return n; n++; } } public static IEnumerator<int> GenerateAndFilterPrime(IEnumerator<int> nums) { // Generate the prime number... nums.MoveNext(); var prime = nums.Current; yield return prime; // ...and return all numbers not a factor of this prime while (nums.MoveNext()) { var n = nums.Current; if (n % prime != 0) yield return n; } } public static IEnumerable<int> Primes() { var p = GenerateAndFilterPrime(NaturalNumbers()); while (p.MoveNext()) { yield return p.Current; p = GenerateAndFilterPrime(p); } } public static void Main (string[] args) { Console.WriteLine(Primes().Skip(99999).First()); } } }

Dold text

Det hela tog 1 minut och 37 sekunder att köra på en i7-2760QM (laptop 2.4GHz).

Edit: ska vara 1m37s och inte 1.37s...

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Var ett tag sedan jag gjorde något i C#, så här är en lite annorlunda variant av att räkna primtal ...

Kul! Jag satt också och funderade på att göra en C#-version med enumeratorer. Har även funderingar på en Stream-implementering i Scala. En Stream är en lat lista så angreppssättet påminner om C# men till skillnad från C# är en Stream mer flexibel, och därtill en del av standard-libbet istället för att vara en språkkonstruktion.

Streams kan å andra sidan vara lite rörigare att resonera kring..

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem

Enligt wikipedia gör du en trial division och inte en Sieve of Eratosthenes, eller? Kan vara värt att påpeka ifall någon blir förvirrad

Till problemet så. Jag försökte skriva ihop något i (T-)SQL, men det var lite klurigt o få det tillräckligt snabbt, och eftersom jag bara försökt så sent på kvällen (som nu tex), så hinner jag inte lägga ner så mkt tid och fick lägga ner iden om en cool SQL-lösning.. men om jag orkar/hinner ska jag ge det ett försök till senare!

Istället implementerade jag en "äkta" Sieve of Eratosthenes i python, direkt från pseudo-koden på wikipedia med den lilla optimeringen att min array bara lagrar udda tal. På min stationära med en i5 2.67 GHz den klart på 6.2 sek.

import ctypes import math def main_ctypes(): n = 10000000 A = (ctypes.c_bool * n)() for x in range(n): A[x] = True for i in range(3, int(math.sqrt(n*2)), 2): if A[i/2]: for j in range(i*3, n*2, i*2): A[j/2] = False # Now all i such that A[i] is true are prime. largest_prime = 2 prime_index = 1 for i in range(3, n*2, 2): if A[i/2]: prime_index += 1 largest_prime = i if prime_index == 1000000: print largest_prime return print "array not large enough"

Dold text

Testade med både cpython o pypy och intressant nog är pypy är ca 3 gånger långsammare än cpython på denna kod. Antar att det beror på att jag använde ctypes för arrayen, och ctypes är ganska slött i pypy har jag märkt..

Jag gjorde även en mer eller mindre copy paste från din Clojure-lösning i python, men den var alldeles för långsam för att jag skulle orka köra den klart

Permalänk
Medlem
Skrivet av Yoshman:

Och här är en logiskt sett nära identisk lösning i Clojure. Denna lösning använder sig just av "lazy evaluation", men kan inte använda en extra CPU-kärna som Go.

(defn prime? [n pr-lst] "Returns true if n is a prime number" (let [limit (int (Math/sqrt n))] (every? #(< 0 (rem n %)) (take-while #(>= limit %) pr-lst)))) (defn primes "Returns a lazy sequence of all prime numbers" ([] (cons 2 (primes 3 []))) ([n ps] (if (prime? n ps) (let [new-ps (conj ps n)] (lazy-seq (cons n (primes (+ n 2) new-ps)))) (recur (+ n 2) ps)))) (println (first (drop 999999 (primes))))

I detta fall skapar funktionen primes en oändlig lista av alla primtal. De första 999,999 primtalen droppas med drop och det första talet i den kvarvarande listan skrivs ut.

Funktionen primes skapar en "lat" sekvens av tal m.h.a lazy-seq och det är en svansrekursiv funktion som anropar sig själv genom nykelordet recur. primes är även en polymorfisk funktion som kan anropas med noll argument eller två argument, nästa tal att testa som primtal och en lista av de primtal man hittat så här långt. lazy-seq ser till att inte köra sin "kropp" oftar än att så många nummer man frågat efter kommer att produceras, så denna funktion kommer inte dra 100% CPU hela tiden och äta upp allt minne. Well, i alla fall inte så länge man försöker t.e.x skriva hela listan som skapas av primes

Dold text

Denna lösning tog 48.6s att köra på laptop:en med 2760QM och 64-bitars Ubuntu 12.10.

Du borde skriva (cons n (lazy-seq ...)) annars räknar du farm ett primtal för mycket i onödan hela tiden.

Permalänk
Medlem

Roligt "progblem", som jag har stött på tidigare!

Försökte mig på att göra det så kort/enkelt som möjligt med linq (väldigt icke-optimalt).

var range = Enumerable.Range(2, 1000000); var primes = (from f in range where !range.TakeWhile(s => s <= Math.Sqrt(f)).Any(s => f % s == 0) select f); Console.WriteLine(primes.Last());

Tror det blev ~4sek på min Core 2 Duo 2.4ghz.

Visa signatur

Utvecklare (Technical Director) / Delägare - Björnmamman

Permalänk
Medlem
Skrivet av Madsoul:

Roligt "progblem", som jag har stött på tidigare!

Försökte mig på att göra det så kort/enkelt som möjligt med linq (väldigt icke-optimalt).

var range = Enumerable.Range(2, 1000000); var primes = (from f in range where !range.TakeWhile(s => s <= Math.Sqrt(f)).Any(s => f % s == 0) select f); Console.WriteLine(primes.Last());

Tror det blev ~4sek på min Core 2 Duo 2.4ghz.

Optimerade den ett snäpp. finns nog mer att göra på den

var range = Enumerable.Range(2, 1000000).Where(f => f == 2 || f % 2 != 0).ToList(); var primes = from f in range.AsParallel() where !range.TakeWhile(s => s <= Math.Sqrt(f)).Any(s => f % s == 0) select f;

Skriver jag enbart ut antal primtal hittade så tar det bara 0,25s för min 3,4Ghz I7. Inte Optimalt? Tja jag vill påstå att det är väldigt optimalt.

Ska man skriva ut varje resultat kommer det dock ta längre tid på grund av tiden det tar att skicka ut allt till konsol men aja.

Edit Tja blev ännu mer optimerat att tja skapa upp en range igen och ignorera TakeWhile

var primes = from f in Enumerable.Range(2, 1000000).Where(f => f == 2 || f % 2 != 0).AsParallel() where Enumerable.Range(2, (int)Math.Sqrt(f)).All(s => f % s != 0) select f;

Denna tar ~0.2s

Visa signatur

Speldator: Ryzen 7800X3D, 64GB DDR5, RTX 5090
Server: i7-8700k, 32GB DDR4, RTX2080
Steam deck, Rog Ally + de fiesta konsoler.

Permalänk
Medlem
Skrivet av MugiMugi:

Optimerade den ett snäpp. finns nog mer att göra på den

var range = Enumerable.Range(2, 1000000).Where(f => f == 2 || f % 2 != 0).ToList(); var primes = from f in range.AsParallel() where !range.TakeWhile(s => s <= Math.Sqrt(f)).Any(s => f % s == 0) select f;

Skriver jag enbart ut antal primtal hittade så tar det bara 0,25s för min 3,4Ghz I7. Inte Optimalt? Tja jag vill påstå att det är väldigt optimalt.

Ska man skriva ut varje resultat kommer det dock ta längre tid på grund av tiden det tar att skicka ut allt till konsol men aja.

Edit Tja blev ännu mer optimerat att tja skapa upp en range igen och ignorera TakeWhile

var primes = from f in Enumerable.Range(2, 1000000).Where(f => f == 2 || f % 2 != 0).AsParallel() where Enumerable.Range(2, (int)Math.Sqrt(f)).All(s => f % s != 0) select f;

Denna tar ~0.2s

Fast ingen av lösningarna tar fram det miljonte primtalet, utan de skriver ut det sista primtalet som är mindre än en miljon..

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem
Skrivet av Teknocide:

Fast ingen av lösningarna tar fram det miljonte primtalet, utan de skriver ut det sista primtalet som är mindre än en miljon..

Snabb som tusan!

Console.WriteLine("15485863");

1.000.001 var inte rätt...
Visa signatur

Utvecklare (Technical Director) / Delägare - Björnmamman

Permalänk
Medlem
Skrivet av Teknocide:

Fast ingen av lösningarna tar fram det miljonte primtalet, utan de skriver ut det sista primtalet som är mindre än en miljon..

primes.Skip(1000000).Take(1);

Om du nu vill ha det miljonte. Åt andra sidan går det inte alls lika fort längre. Dvs ~ 27s.

Visa signatur

Speldator: Ryzen 7800X3D, 64GB DDR5, RTX 5090
Server: i7-8700k, 32GB DDR4, RTX2080
Steam deck, Rog Ally + de fiesta konsoler.

Permalänk
Medlem
Skrivet av Madsoul:

Snabb som tusan!

Console.WriteLine("15485867");

Jag får det till 15485863

Skrivet av MugiMugi:

primes.Skip(1000000).Take(1);

Om du nu vill ha det miljonte. Åt andra sidan går det inte alls lika fort längre. Dvs ~ 27s.

Det ger en tom enumerator här. range går ju bara upp till 1000000, den måste vara minst lika stor som primtalet man är ute efter. Moment 22. (och ja, algoritmen blir jäkligt seg.. )

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Datavetare
Skrivet av tufflax:

Du borde skriva (cons n (lazy-seq ...)) annars räknar du farm ett primtal för mycket i onödan hela tiden.

Du har helt rätt, kommer däremot inte göra en mätbar skillnad i hastighet i just detta fall.
Och jag skulle alltid kunna hävda att den skulle vara exakt som Go varianten som alltid ligger ett primtal före
Fast i Go fallet är finns det en poäng då main och primtalsberäkningen sker i olika goroutines och därmed kan ske parallellt.

En av anledningarna till att jag började titta på Clojure var just att själva språket lämpar sig väldigt väl för "concurrent programming", tyvärr är runtime-miljön allt annat än optimal för massiv I/O då man ska använda send-off i stället för send (Clojure agents) om man ska göra potentiellt blockande I/O. send-off skapar en ny Java-tråd, som i sin tur är är 1:1 mappad till OS-trådar så det skalar uselt, kör man >10000 send-off så får man 10000 trådar och systemet kommer gå i snigelfart.

I Go mappas alla goroutinesN stycken OS-trådar där N normalt sett är lika många som man har CPU-trådar. Det är därför inget problem att t.ex. hantera 100.000 sockets, var och en på sin egen goroutine. Detta är en anledning varför jag börjar gilla Go allt mer, att Go typiskt är snabbare än C#/Java och ligger väldigt nära C och C++ i prestanda är ju inte precis någon nackdel

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare
Skrivet av vb:

Enligt wikipedia gör du en trial division och inte en Sieve of Eratosthenes, eller? Kan vara värt att påpeka ifall någon blir förvirrad

Du har helt rätt och jag var otydligt i första inlägget i att jag inte använder Sieve of Eratosthenes i samband med att jag nämner att den beräknar alla primtal upp till en viss gräns medan detta problem var att beräkna det miljonte primtalet.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare

Sieve of Eratosthenes

Här kommer en riktigt implementation som använder Sieve of Eratosthenes, det går lite snabbare än tidigare lösningar

#include <iostream> #include <vector> #include <cmath> #define UP_TO 20000000 using namespace std; vector<bool> calc_primes(void) { unsigned i; unsigned j; const unsigned limit = ceil(sqrt(UP_TO)); // Initial mark all numbers as prime vector<bool> primes(UP_TO, true); // Go through the vector of primes and delete all numbers that are // a multiple of a prime number for (i = 2; i < limit; i++) if (primes[i]) for (j = 2 * i; j < UP_TO; j += i) primes[j] = false; return primes; } unsigned nth_prime(const vector<bool> &primes, unsigned nth) { unsigned n = 1; unsigned p = 2; while (n < nth) if (primes[++p]) n++; return p; } int main() { cout << nth_prime(calc_primes(), 1000000) << endl; }

Dold text

Denna tar 0.10s kompilerad med g++ 4.7 under Ubuntu 12.10 körandes på en i7-2760QM CPU @ 2.40GHz

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare

Java

Och bara för att visa hur mycket snabbare Go är än Java så kommer här en variant som använder samma grundläggande algoritm som min Go och Clojure lösning.

import java.util.ArrayList; class Prime { private ArrayList<Integer> primes = new ArrayList<Integer>(); private boolean isPrime(int n) { int limit = (int) Math.sqrt(n); for (int i = 0; i < limit; i++) if (n % primes.get(i) == 0) return false; return true; } private void calcPrimes(int cnt) { int n; if (primes.size() == 0) primes.add(2); n = primes.get(primes.size() - 1); while (primes.size() < cnt) { if (isPrime(++n)) primes.add(n); } } public int nth(int n) { if (primes.size() < n) calcPrimes(n); return primes.get(n-1); } public static void main(String[] args) { Prime p = new Prime(); System.out.println(p.nth(1_000_000)); } }

Dold text

Tar 11.2s med Java7 (OpenJDK) under Ubuntu 12.10 på en i7-2760QM CPU @ 2.40GHz

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Komplett lösning i Scala:

object Primes extends App { import System.{currentTimeMillis => now} def isPrime(n: Int): Boolean = { val target = math.sqrt(n) primes takeWhile (_ <= target) forall (n % _ != 0) } val primes = 2 #:: (Stream.from(3, 2) filter isPrime) val startTime = now val result = primes drop 999999 take 1 val endTime = now println ( """|Det miljonte primtalet är %d |Beräkningen tog %d millisekunder""".stripMargin.format(result.head, endTime - startTime) ) }

Dold text

Output:

Citat:

Det miljonte primtalet är 15485863
Beräkningen tog 27247 millisekunder

Lite förvånad över att det tar sån tid men jag gissar att det är en kombination av Stream-celler som skapas och filter-/forall-delegaterna

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Hedersmedlem
Skrivet av Yoshman:

Denna tar 0.10s kompilerad med g++ 4.7 under Ubuntu 12.10 körandes på en i7-2760QM CPU @ 2.40GHz

Och då testas ju ändå 30% fler tal än nödvändigt...

Permalänk
Medlem
Skrivet av Teknocide:

Jag får det till 15485863

Det ger en tom enumerator här. range går ju bara upp till 1000000, den måste vara minst lika stor som primtalet man är ute efter. Moment 22. (och ja, algoritmen blir jäkligt seg.. )

Nja självklart får du göra lite mer mer väldigt lätt mod så :P, Hade bråttom när jag skrev det.

var result = (from f in Enumerable.Range(2, int.MaxValue - 3).Where(f => f == 2 || f % 2 != 0).AsParallel() where Enumerable.Range(2, (int)Math.Sqrt(f)).All(s => f % s != 0) select f).Skip(999999).Take(1).FirstOrDefault();

Denna tog bara förvisso ~6s, inte så illa för en brutal force. Kunde gjort den lättare att förstå men då får man inga av de lättare prestanda sätten att göra.

Visa signatur

Speldator: Ryzen 7800X3D, 64GB DDR5, RTX 5090
Server: i7-8700k, 32GB DDR4, RTX2080
Steam deck, Rog Ally + de fiesta konsoler.

Permalänk
Datavetare
Skrivet av Teknocide:

Lite förvånad över att det tar sån tid men jag gissar att det är en kombination av Stream-celler som skapas och filter-/forall-delegaterna

Är å enda sidan inte alls förvånad, Scala brukar ju hamna mellan Java och Clojure om man skriver program på ett sätt som utnyttjar respektive språks styrkor.

Men å andra sidan så testade jag att skriva ett C-program på ett sätt som borde ungefär efterlikna hur de Linq och din Scala lösning borde representeras och fungera under huven. Men C-programmet tog endast 2.2s att köra på en CPU-kärna medan Scala-programmet definitivt använder flera CPU-kärnor. Enda sättet at förklara en sådan stor skillnad mellan C och både Scala och C#/Linq lösningarna är att de senare skapar massor med kortlivade temporära objekt under huven. Sådant sätter enorm press på skräpsamlaren, vilket i sin tur havererar skalbarheten över flera CPU-kärnor (noterade AsParallel i Linq lösningarna).

I mitt fall tog det 52.1s att köra Scala programmet om jag låser det till en CPU-kärna och 22.2s om det får använda alla CPU-trådar på min Core i7 2600 @ 3.4GHz (alltså inte den laptop jag använde tidigare). Scala-programmet har en hel del overhead när den får använda alla trådar, total CPU-tid för programmet när man kör alla trådar var 78.9s, vilket när man jämför det med fallet när den bara fick använda en CPU-kärna visar att strax över 50% av använd CPU-tid är ren overhead.

Här är C programmet i alla fall, blir rätt långt om man ska lösa det på Scala / Linq vis...

#include <math.h> #include <stdio.h> #include <stdlib.h> struct prime { struct prime *next; unsigned prime; int (*fn)(struct prime *, unsigned n); }; struct iterator { unsigned n; struct prime *primes; struct prime **last; }; int is_multiple(struct prime *prime, unsigned n) { return n % prime->prime == 0; } struct iterator * new_prime_iter(void) { struct iterator *it = malloc(sizeof *it); it->n = 2; it->primes = NULL; it->last = &it->primes; return it; } void delete_prime_iter(struct iterator *it) { struct prime *p; struct prime *next_p; for (p = it->primes; p; p = next_p) { next_p = p->next; free(p); } free(it); } unsigned next_prime(struct iterator *it) { struct prime *filter; unsigned n = it->n; do { unsigned limit = ceil(sqrt(n)); for (filter = it->primes; filter; filter = filter->next) { if (filter->prime > limit) { /* Prime */ filter = NULL; break; } if (filter->fn(filter, n)) /* Not a prime */ break; } if (filter == NULL) { filter = malloc(sizeof *filter); filter->fn = is_multiple; filter->prime = n; filter->next = NULL; /* Insert this filter last */ *it->last = filter; it->last = &filter->next; it->n = n + 1; return n; } } while (++n); } int main() { unsigned skip = 999999; struct iterator *prime_iter = new_prime_iter(); while (skip-- > 0) next_prime(prime_iter); printf("%u\n", next_prime(prime_iter)); delete_prime_iter(prime_iter); }

Dold text
Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Är å enda sidan inte alls förvånad, Scala brukar ju hamna mellan Java och Clojure om man skriver program på ett sätt som utnyttjar respektive språks styrkor.

Men å andra sidan så testade jag att skriva ett C-program på ett sätt som borde ungefär efterlikna hur de Linq och din Scala lösning borde representeras och fungera under huven. Men C-programmet tog endast 2.2s att köra på en CPU-kärna medan Scala-programmet definitivt använder flera CPU-kärnor. Enda sättet at förklara en sådan stor skillnad mellan C och både Scala och C#/Linq lösningarna är att de senare skapar massor med kortlivade temporära objekt under huven. Sådant sätter enorm press på skräpsamlaren, vilket i sin tur havererar skalbarheten över flera CPU-kärnor (noterade AsParallel i Linq lösningarna).

I mitt fall tog det 52.1s att köra Scala programmet om jag låser det till en CPU-kärna och 22.2s om det får använda alla CPU-trådar på min Core i7 2600 @ 3.4GHz (alltså inte den laptop jag använde tidigare). Scala-programmet har en hel del overhead när den får använda alla trådar, total CPU-tid för programmet när man kör alla trådar var 78.9s, vilket när man jämför det med fallet när den bara fick använda en CPU-kärna visar att strax över 50% av använd CPU-tid är ren overhead.

Scala-koden jag skrev är helt singeltrådad.
Om jag skulle göra en implementation likt den du gjort i Java skulle den med stor sannolikhet vara en god bit
snabbare då Java tampas med boxing av integers.

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem
Skrivet av MugiMugi:

var result = (from f in Enumerable.Range(2, int.MaxValue - 3).Where(f => f == 2 || f % 2 != 0).AsParallel() where Enumerable.Range(2, (int)Math.Sqrt(f)).All(s => f % s != 0) select f).Skip(999999).Take(1).FirstOrDefault();

Har märkt att min jobdator totalvägrar att köra denna kod parallelt, även med WithDegreeOfParallelism men min burk hemma har liksom inga problem alls. Önskar man kunde tvinga PLINQ lite mer men tja värkar inte som det. Jobb dator tar ~26s att köra igenom det på grund av att den inte klarar det trådat. en i7 Ivy-bridge burk btw samt min burk i sig är burken hemma.

Visa signatur

Speldator: Ryzen 7800X3D, 64GB DDR5, RTX 5090
Server: i7-8700k, 32GB DDR4, RTX2080
Steam deck, Rog Ally + de fiesta konsoler.

Permalänk
Medlem
Skrivet av MugiMugi:

Har märkt att min jobdator totalvägrar att köra denna kod parallelt, även med WithDegreeOfParallelism men min burk hemma har liksom inga problem alls. Önskar man kunde tvinga PLINQ lite mer men tja värkar inte som det. Jobb dator tar ~26s att köra igenom det på grund av att den inte klarar det trådat. en i7 Ivy-bridge burk btw samt min burk i sig är burken hemma.

Och dessutom ger den också det 1,000,001:a primtalet

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem
Skrivet av Teknocide:

Och dessutom ger den också det 1,000,001:a primtalet

Jo la märke till det precis oxo men fixad. samt lärt mig något nytt antående PLINQ http://msdn.microsoft.com/en-us/library/dd547138.aspx kommer läggas på minnet, så ja

var primes = (from f in Enumerable.Range(2, int.MaxValue - 3).Where(f => f == 2 || f % 2 != 0).AsParallel().AsOrdered().WithExecutionMode(ParallelExecutionMode.ForceParallelism) where Enumerable.Range(2, (int)Math.Sqrt(f) - 1).All(s => f % s != 0) select f).Skip(999999).Take(1).FirstOrDefault();

Denna tog ~6s på jobb datorn. Och ja vist finns det låsningar märker man, går inte precis 8 ggr fortare men betydligt fotrare en singel trådad ( 27s )

Visa signatur

Speldator: Ryzen 7800X3D, 64GB DDR5, RTX 5090
Server: i7-8700k, 32GB DDR4, RTX2080
Steam deck, Rog Ally + de fiesta konsoler.

Permalänk
Datavetare
Skrivet av Teknocide:

Scala-koden jag skrev är helt singeltrådad.
Om jag skulle göra en implementation likt den du gjort i Java skulle den med stor sannolikhet vara en god bit
snabbare då Java tampas med boxing av integers.

Jo jag ser att den är enkeltrådad, men Scala gör något i bakgrunden som leder till att man får i snitt 350-400% CPU last på en Core i7 2600. Vet inte hur Windows mäter detta, men på UNIX så betyder 100% att man använder en CPU fullt ut så scala (eller egentligen programmet java) anser tydligen att det finns något man kan lägga ut på flera trådar. Bl.a. GC körs ju potentiellt på egna trådar.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare
Skrivet av MugiMugi:

Jo la märke till det precis oxo men fixad. samt lärt mig något nytt antående PLINQ http://msdn.microsoft.com/en-us/library/dd547138.aspx kommer läggas på minnet, så ja

var primes = (from f in Enumerable.Range(2, int.MaxValue - 3).Where(f => f == 2 || f % 2 != 0).AsParallel().AsOrdered().WithExecutionMode(ParallelExecutionMode.ForceParallelism) where Enumerable.Range(2, (int)Math.Sqrt(f) - 1).All(s => f % s != 0) select f).Skip(999999).Take(1).FirstOrDefault();

Denna tog ~6s på jobb datorn. Och ja vist finns det låsningar märker man, går inte precis 8 ggr fortare men betydligt fotrare en singel trådad ( 27s )

Går faktiskt att lösa detta mer eller mindre på exakt samma sätt i C++ m.h.a. CppLinq.

#include <cmath> #include <iostream> #include <vector> #include "cpplinq.hpp" using namespace std; using namespace cpplinq; int main() { int prime = (range(2, numeric_limits<int>::max() - 3) >> where([](int f){return (f == 2 || f % 2 != 0 && (range(2, sqrt(f) - 1) >> all([=](int s){return f % s != 0;})));}) >> skip(999999) >> take(1) >> to_vector()).front(); cout << prime << endl; }

Dold text

Att köra det hela tog 12.1s på min laptop med i7-2760QM CPU @ 2.40GHz

Det finns ingen C++ motsvarighet till parallell Linq vad jag vet, men man kan skriva om programmet t.ex. så här

include <cmath> #include <iostream> #include <omp.h> #include <vector> #include "cpplinq.hpp" using namespace std; using namespace cpplinq; int main() { const int nth = 1000000; const int stride = nth / omp_get_max_threads(); int n = 2; vector<int> primes; vector<vector<int> > slices(omp_get_max_threads()); do { int id = 0; #pragma omp parallel { int from; int to; vector<int> *pslice; #pragma omp critical { from = n; to = n + stride; n += stride; pslice = &slices[id++]; } *pslice = move(range(from, to - from) >> where([](int f){return (f == 2 || f % 2 != 0 && (range(2, sqrt(f) - 1) >> all([=](int s){return f % s != 0;})));}) >> to_vector()); } for (vector<int> slice: slices) primes.insert(end(primes), begin(slice), end(slice)); } while (primes.size() < nth); cout << primes[nth-1] << endl; }

Dold text

Tar 12.6s på en CPU-kärna, vilket är en ökning från 12.1s i versionen ovan men det får man räkna med då det blir en viss overhead att bara ha möjligheten att köra på flera CPU-kärnor.

Tar 6.6s på två kärnor -> x1.9
Tar 4.6s på tre kärnor -> x2.7
Tar 3.5s på fyra kärnor -> x3.6

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer