Skrivet av MugiMugi:
Det är inte nätverks I&O som sätter stop, utan disken, dvs flashminne skrivhastighet som är den stora flaskhalsen. Prestandan på enheten är långt ifrån lika bra från några av de enheter du beskriver, vi kör inte ens på en multicore ARM processor utan den har en kärna.
Vilket var exakt det fall jag tycker .Net fungerar bra: det som utförs av koden körandes under .Net är så pass trivial att alla eventuella flaskhalsar ligger på andra ställen. Min erfarenhet här är att detta inte händer, om CPU-delen inte är flaskhals i ett inbyggt-system så tar man än billigare CPU för när man gör saker i stora mängder så är alla sådana förändringar direkt extra inkomst då det kan tas ut i form av bättre marginal.
Är det som gör det kul att jobba med embedded, slår man ut kostnaden för mig som programmerare över alla produkter där programvaran ska köras så går det att lägga väldigt mycket pengar på ett mer effektivt program då det betyder lägre BoM på enheten man bygger med bibehållen funktion! Är därför långt viktigare att skriva effektiva program än att det går lite snabbare att skriva programmet.
Dagens JVMer är rejält mycket mer effektivare än .Net även på en CPU-kärna i program som av någon anledning behöver skapa väldigt mycket kortlivade objekt.
Nedan är koden för ett extremt ineffektivt sätt att räkna Fibonacci. Men det är ändå något som innehåller intressant information då den rekursiva Fibonacci algoritmer har extremt mycket potentiell parallellism i sig. Det i sin tur går att använda för att mäta "overhead" i olika ramverks sätt att utföra s.k. fork-join, något som TPL, Java fork-join, OpenMP (tasks), Cilk+ m.fl. har explicit stöd för.
På min dator med i7-4702MQ körandes Win8.1 är Java8 (Oracles JVM) ca 10 gånger snabbare än .Net4.5 (VS2013) i fallet där en CPU-kärna används, är ca 50k kortlivade objekt som skapas under en körning, gissar att Java hanterar en stor andel av dessa via s.k. escape analysis (finns sedan Java6) något .Net än så länge saknar stöd för.
Det går att få .Net att skala relativt bra genom att sätta PARALLEL_THRESH till 30-35 då ett värde på 20 ger lite väl "mycket" potentiell parallellism -> förstärker alla seriella delar i ramverket kring att starta "tasks" och samla in deras delresultat (de används ju som promises kring att i framtiden leverera resultat för den beräkning de representerar). Med ett PARALLEL_THRESH värde på 20 ger .Net endast en "speed up" över 4 kärnor / 8 trådar på 1.1 (d.v..s 1.1 gånger snabbare när alla CPU-trådar används) medan Java8 ger en "speed up" på 3.9 med samma parametrar.
Får gärna förslå andra sätt man kan använda TPL för att göra detta mer effektivt, d.v.s. det ska vara en ineffektiv algoritm då det är ramverket man vill stressa/testa.
Java
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class Fib extends RecursiveTask<Integer> {
public static void main(String[] args) throws InterruptedException, ExecutionException {
int n = 42;
System.out.printf("Speedup %.1f\n", pfib(n, 1) / pfib(n, 8));
}
static double measure(Runnable r) {
long start = System.currentTimeMillis();
r.run();
return (System.currentTimeMillis() - start) / 1000.0;
}
static double pfib(int n, int concurrency) {
final ForkJoinPool pool = new ForkJoinPool(concurrency);
final Fib f = new Fib(n);
Runnable fn = () -> { pool.invoke(f); System.out.println(f.value()); };
double duration = measure(fn);
System.out.printf("Took %.2fs\n", duration);
return duration;
}
private static final long serialVersionUID = 1L;
final int n;
static final int PARALLEL_THRESH = 20;
Fib(int n) {
this.n = n;
}
protected Integer compute() {
if (n <= PARALLEL_THRESH) {
return serialFib(n);
}
Fib f1 = new Fib(n-1);
f1.fork();
int f2 = new Fib(n-2).compute();
return f2 + f1.join();
}
int serialFib(int n)
{
if (n <= 1) {
return n;
}
return serialFib(n-1) + serialFib(n-2);
}
protected Integer value()
{
Integer v = 0;
try {
v = get();
} catch (Exception e) {
e.printStackTrace();
}
return v;
}
}
Dold text
C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
namespace Fib
{
// Source for LimitedConcurrencyLevelTaskScheduler should go here
// Can be found at: https://msdn.microsoft.com/en-us/library/ee789351%28v=vs.110%...
class Program
{
static TaskFactory cltf;
static double measure(Action fn)
{
var start = DateTime.Now;
fn();
return (DateTime.Now - start).TotalSeconds;
}
static int SerialFib(int n)
{
if (n <= 1)
return n;
return SerialFib(n - 1) + SerialFib(n - 2);
}
static int Fib(int n)
{
if (n <= PARALLEL_THRESH)
return SerialFib(n);
var f1 = cltf.StartNew(() => Fib(n - 1));
var f2 = Fib(n - 2);
return f1.Result + f2;
}
const int PARALLEL_THRESH = 20;
static double Pfib(int n, int concurrency)
{
// Source for LimitedConcurrencyLevelTaskScheduler:
// https://msdn.microsoft.com/en-us/library/ee789351%28v=vs.110%...
cltf = new TaskFactory(new LimitedConcurrencyLevelTaskScheduler(concurrency));
Action fn = () => Console.WriteLine("Fib({0}) = {1}", n, Fib(n));
var duration = measure(fn);
Console.WriteLine("Took {0:F2}s", duration);
return duration;
}
static void Main(string[] args)
{
var n = 42;
Console.WriteLine("Speedup {0:F1}", Pfib(n, 1) / Pfib(n, 8));
Console.In.Read();
}
}
}
Dold text
Edit: C#/.Net gör tydligen inte ens de mest uppenbara optimeringar för att undvika extra kostnader i funktionsprologer/epiloger. "static" deklarera saker ger helt klart boost i C# men gör ingen skillnad i Java. Notera att rent logiskt skapar Java nu runt dubbel så mycket objekt på "heapen" men är ändå snabbare (garanterat p.g.a. "escape analysis"). D.v.s. C#/.Net lider betydligt mer av s.k. leaky abstraction jämfört med Java/JVM här. Om det är ett mer allmängiltigt problem (inte alls otroligt) så är det kanske marginell enklare att skriva något i C# men om programmet faktiskt ska utföra en krävande uppgift blir det rejält mycket enklare att nå kraven i Java.
Strukturerade om lite i C# programmet, nu skalar det dels klart bättre med en "speed up" på 2.5 samt att den seriella versionen av Java är nu "bara" 4 gånger snabbare. Marginell förbättring i Java-delen, "speed up" nu på 4.1 (4 kärnor + HT så teoretiskt är det möjligt att nå en bit över x5).