Skrivet av Teknocide:
Angående kodexemplet, det finns overhead men det går inte att jämföra statiskt kompilerad C-kod med ett språk som optimeras under runtime. JITtern arbetar med bytekoden under runtime vilket jag är säker på att du känner till och skillnaden mellan iterationer är påtaglig. Antalet kortlivade objekt som skapas är också få och bör inte ha märkbar effekt på prestandan.
Det du skriver om dynamisk re-JIT är sant för Java (JVM) men inte sant för C# (CLR). CLR har bara en one-time JIT och kan därför inte utföra vissa typer av optimeringar som senare kan behöva tas bort. CLR saknar (i alla fall i version 5.0) även s.k. escape-analysis, något som i vissa fall gör det möjligt att lägga kortlivade objekt på stacken i de fall runtime:en kunnat garantera att de aldrig kan "fly" från aktuellt kontext. Detta syns numera rätt väl om man jämför motsvarande program skrivet i Java och C#, Java7 (efter JIT-warmup) tenderar att vara 30-40% snabbare än motsvarande C#4.5 program.
Så i C#/LINQ får du exakt alla de kostnader jag beskrev, hade Java haft LINQ så hade vissa varit möjliga att undvika. Nackdelen med Javas mer aggressiva JIT:ande är att det typiskt går åt mer CPU-cykler initialt + att mängden RAM som går åt är mycket högre. Verkar som MS aktivt valt att inte implementera detta i .Net då man vill att detta ska kunna användas både i relativt kortlivade och interaktiva program (desktop-program) samt även på servers. Java verkar bara rikta in sig på serversidan idag.
Och trots alla dessa optimeringar har jag fortfarande aldrig sett ett icke-trivialt (d.v.s något mer avancerat en än microbenchmark) som visar att Java är snabbare än motsvarande program skrivet i C eller C++. Jag hävdar däremot inte att Java är långsamt, tvärtom är dagens JVM:er riktigt riktigt bra!
Skrivet av Teknocide:
(kodsnutten kan förresten göras ännu mer elegant genom att direkt anropa string.IsNullOrEmpty som en delegate:
nameList.Where(string.IsNullOrEmpty).Count();
Det ser näst intill ut som ren engelska skriven av någon som tagit fel på punkt- och mellanslagstangenten..)
Och med den omskrivning blir det också explicit att man använder dynamisk dispatch som CLR inte kan optimera bort, men >=Java6 (möjligen Java5) skulle eventuellt kunna skriva en sådant anrop till att använda statisk dispatch.
Skrivet av Teknocide:
LINQ-satsen fungerar på alla IEnumerables och inte bara Array-liknande datastrukturer. Den är kortare och enklare att både läsa och skriva vilket innebär mindre chans för en bug (du skrev t ex i istället för index).
Jag skrev ingen koden förutom Clojure exemplet, allt var cut-and-paste. Och då det är ett statiskt typat och komplierat språk hade man ändå hittat det direkt + att den typen av felskrivningar var definitivt inget jag märkte att helt försvann när man programmerar språk som Haskell, Scala eller Clojure varav det kan bli ganska svåra fel att hitta i Clojure som är dynamiskt typat.
Skrivet av Teknocide:
Implementationen av .Where och .Count är inte given: Om den i dag innebär overhead skulle den imorgon lika gärna kunna resultera i bytecode motsvarande low level-loopen du visade (bortsett från string.IsNullOrEmpty, som dock kommer inlinas). Ju högre abstraktionsnivån blir desto större möjligheter finns det för den underliggande arktitekturen att göra osynliga men påtagliga optimeringar. for-loopen å andra sidan kommer förbi en for-loop; en lågnivåkonstruktion som dikterar sin egen funktion.
I C#/CLR: nej. Kompilera detta och titta i bytekoden (se nedan, jag stoppade in uttrycket i en statisk metod som jag kallade "bar"), funktionen nedan är en automatiskt skapad anonym funktion (d.v.s "name => string.IsNullOrEmpty(name)") anropet till string.IsNullOrEmpty är inte inline, däremot ser man att det är en metod som är statiskt definierad i klassen string.
.method private static hidebysig
default bool '<bar>m__0' (string name) cil managed
{
.custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ....
// Method begins at RVA 0x2168
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call bool string::IsNullOrEmpty(string)
IL_0006: ret
}
I Java: kanske, men det är lång rad krav som måste vara uppfylld innan re-JIT kan utföra de lite mer aggressiva optimeringarna.
Skrivet av Teknocide:
C# är ett brett språk på så vis att det möjliggör insteg i både lågnivå och högnivå, men det säljs och används i störst utsträckning som ett högnivåspråk så varför inte använda det som ett sådant? LINQ är en av de mest intressanta egenskaperna .NET-plattformen har (reified generics som folk verkar tokiga i tycker jag är ganska ointressant..) och det är grundläggande sedan version 3.5. Hela jäkla collections-delen av standardbiblioteket använder sig av det och många andra delar också.
Varför man inte kan (eller i alla fall inte bör) använda alla funktioner och finesser i ett så stor språk som C# och en så stor plattform som .Net inser man så fort det används i riktigt stora projekt, det fungerar inte att använda mer en relativt enkel delmängd. Felet med C#/.Net är kanske att det är för stort och komplicerat idag, Java (språket) börjat tyvärr också lockas av att plocka in för mycket. Stora projekt betyder många människor, många människor betyder många stilar och framförallt väldigt varierande kunskapsnivå. Ett sätt att hålla saker under någorlunda kontroll är att hindra ALLA från att använda de mest kraftfulla (och därmed "farliga") verktygen.
Titta på företag som Microsoft, Google och Facebook eller rättare sagt titta på deras kod-standard: de är alla företag som kör C++ på de mest kritiska komponenterna, men det är en extremt simplifierad variant av C++. Microsoft säger ju själva att de kör med C-with-classes. Googles kodstandard kan du hitta med t.ex. Google. Jobbar själv på ett företag som har kodbaser på många miljoner rader som används i prestandakritiska system, vi har precis börja tillåta C99 (körde C89 fram till två år sedan). Vi har inget förbud mot att använda "avancerade" saker som funktionspekare eller rekursion, men om du använder dessa så kommer du behöva förklara varför det är en bra idé. Framförallt rekursion kräver att man kan visa att djupet har ett väldefinierat max och att detta max är OK givet den stack man har på systemet.
Edit: det slog mig att jag faktisk bara visade att den genererade CIL inte inline:ar något. Men tar man upp den JIT:ade assemblern så ligger det faktiskt 2 call opkoder i den funktionen, den ena är till adressen för string::IsNullOrEmpty, men den andra (som sker två rader innan) är jag inte helt 100 på varför den behövs, den verkar hämta argumentet på något sätt (översättningen av ldarg.0 ?). Vilket i så fall betyder att en så pass enkel funktion genererar två call i maskinkod.
Ber om ursäkt för att detta gick lite väl OT :/