ประสิทธิภาพมีประโยชน์ไม่ทางใดก็ทางหนึ่งหรือไม่? คอมไพเลอร์ / VM เฉพาะหรือไม่ ฉันใช้ Hotspot
ประสิทธิภาพมีประโยชน์ไม่ทางใดก็ทางหนึ่งหรือไม่? คอมไพเลอร์ / VM เฉพาะหรือไม่ ฉันใช้ Hotspot
คำตอบ:
อันดับแรก: คุณไม่ควรเลือกแบบคงที่และไม่คงที่โดยพิจารณาจากประสิทธิภาพ
ประการที่สอง: ในทางปฏิบัติมันจะไม่สร้างความแตกต่างใด ๆ Hotspot อาจเลือกที่จะเพิ่มประสิทธิภาพในรูปแบบที่ทำให้การโทรแบบคงที่เร็วขึ้นสำหรับวิธีการหนึ่งการโทรแบบไม่คงที่เร็วขึ้นสำหรับอีกวิธีหนึ่ง
ประการที่สาม: Mythos ส่วนใหญ่ที่อยู่รอบ ๆ แบบคงที่และไม่คงที่นั้นขึ้นอยู่กับ JVM ที่เก่ามาก (ซึ่งไม่ได้ทำที่ใดก็ได้ใกล้กับการเพิ่มประสิทธิภาพที่ Hotspot ทำ) หรือบางเรื่องที่จำได้เกี่ยวกับ C ++ (ซึ่งการเรียกแบบไดนามิกจะใช้การเข้าถึงหน่วยความจำอีกหนึ่งครั้งกว่าการโทรแบบคงที่)
สี่ปีต่อมา ...
เอาล่ะด้วยความหวังที่จะตั้งคำถามนี้ครั้งแล้วครั้งเล่าฉันได้เขียนเกณฑ์มาตรฐานซึ่งแสดงให้เห็นว่าการโทรประเภทต่างๆ (เสมือนไม่ใช่เสมือนแบบคงที่) เปรียบเทียบกันอย่างไร
ฉันวิ่งบน ideoneและนี่คือสิ่งที่ฉันได้รับ:
(การทำซ้ำจำนวนมากจะดีกว่า)
Success time: 3.12 memory: 320576 signal:0
Name | Iterations
VirtualTest | 128009996
NonVirtualTest | 301765679
StaticTest | 352298601
Done.
ตามที่คาดไว้การเรียกใช้เมธอดเสมือนเป็นการเรียกใช้เมธอดที่ช้าที่สุดและไม่ใช่วิธีเสมือนจะเร็วกว่าและการเรียกเมธอดแบบคงที่จะเร็วกว่า
สิ่งที่ฉันไม่คาดคิดคือความแตกต่างที่จะออกเสียง: การเรียกเมธอดเสมือนถูกวัดว่าทำงานด้วยความเร็วน้อยกว่าครึ่งหนึ่งของการเรียกเมธอดที่ไม่ใช่เสมือนซึ่งจะถูกวัดเพื่อเรียกใช้ทั้งหมดช้ากว่าการโทรแบบคงที่ 15% นั่นคือสิ่งที่การวัดเหล่านี้แสดงให้เห็น ในความเป็นจริงความแตกต่างที่แท้จริงจะต้องชัดเจนกว่าเล็กน้อยเนื่องจากสำหรับการเรียกใช้เมธอดเสมือนจริงไม่ใช่เสมือนและแบบคงที่แต่ละโค้ดการเปรียบเทียบของฉันมีค่าโสหุ้ยค่าคงที่เพิ่มเติมในการเพิ่มตัวแปรจำนวนเต็มหนึ่งตัวแปรตรวจสอบตัวแปรบูลีนและการวนซ้ำหากไม่เป็นจริง
ฉันคิดว่าผลลัพธ์จะแตกต่างกันไปในแต่ละ CPU และจาก JVM ถึง JVM ดังนั้นลองดูสิว่าคุณจะได้อะไร:
import java.io.*;
class StaticVsInstanceBenchmark
{
public static void main( String[] args ) throws Exception
{
StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
program.run();
}
static final int DURATION = 1000;
public void run() throws Exception
{
doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ),
new NonVirtualTest( new ClassWithNonVirtualMethod() ),
new StaticTest() );
}
void doBenchmark( Test... tests ) throws Exception
{
System.out.println( " Name | Iterations" );
doBenchmark2( devNull, 1, tests ); //warmup
doBenchmark2( System.out, DURATION, tests );
System.out.println( "Done." );
}
void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
{
for( Test test : tests )
{
long iterations = runTest( duration, test );
printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
}
}
long runTest( int duration, Test test ) throws Exception
{
test.terminate = false;
test.count = 0;
Thread thread = new Thread( test );
thread.start();
Thread.sleep( duration );
test.terminate = true;
thread.join();
return test.count;
}
static abstract class Test implements Runnable
{
boolean terminate = false;
long count = 0;
}
static class ClassWithStaticStuff
{
static int staticDummy;
static void staticMethod() { staticDummy++; }
}
static class StaticTest extends Test
{
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
ClassWithStaticStuff.staticMethod();
}
}
}
static class ClassWithVirtualMethod implements Runnable
{
int instanceDummy;
@Override public void run() { instanceDummy++; }
}
static class VirtualTest extends Test
{
final Runnable runnable;
VirtualTest( Runnable runnable )
{
this.runnable = runnable;
}
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
runnable.run();
}
}
}
static class ClassWithNonVirtualMethod
{
int instanceDummy;
final void nonVirtualMethod() { instanceDummy++; }
}
static class NonVirtualTest extends Test
{
final ClassWithNonVirtualMethod objectWithNonVirtualMethod;
NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
{
this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
}
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
objectWithNonVirtualMethod.nonVirtualMethod();
}
}
}
static final PrintStream devNull = new PrintStream( new OutputStream()
{
public void write(int b) {}
} );
}
เป็นที่น่าสังเกตว่าความแตกต่างของประสิทธิภาพนี้ใช้ได้เฉพาะกับโค้ดที่ไม่ทำอะไรเลยนอกจากการเรียกใช้เมธอดแบบไร้พารามิเตอร์ ไม่ว่าคุณจะมีรหัสอื่นใดระหว่างการเรียกใช้จะทำให้ความแตกต่างลดลงและรวมถึงการส่งผ่านพารามิเตอร์ด้วย อันที่จริงความแตกต่าง 15% ระหว่างการโทรแบบคงที่และไม่เสมือนจริงนั้นอาจอธิบายได้ทั้งหมดโดยข้อเท็จจริงที่ว่าthis
ตัวชี้ไม่จำเป็นต้องถูกส่งต่อไปยังวิธีการแบบคงที่ ดังนั้นจะต้องใช้โค้ดเพียงเล็กน้อยในการทำสิ่งเล็กน้อยระหว่างการเรียกร้องให้มีการลดความแตกต่างระหว่างการเรียกประเภทต่างๆจนถึงจุดที่ไม่มีผลกระทบสุทธิใด ๆ
นอกจากนี้ยังมีการเรียกวิธีการเสมือนด้วยเหตุผล; พวกเขามีจุดประสงค์เพื่อให้บริการและถูกนำไปใช้โดยใช้วิธีการที่มีประสิทธิภาพสูงสุดที่จัดหาโดยฮาร์ดแวร์พื้นฐาน (ชุดคำสั่ง CPU) หากคุณต้องการกำจัดโดยการแทนที่ด้วยการเรียกที่ไม่ใช่เสมือนหรือแบบคงที่คุณจะต้องเพิ่มรหัสพิเศษมากถึง iota เพื่อจำลองฟังก์ชันการทำงานของพวกเขาค่าใช้จ่ายสุทธิที่เกิดขึ้นของคุณจะถูกผูกไว้ ไม่น้อย แต่มากขึ้น อาจเป็นไปได้มากมากมากอย่างไม่น่าเชื่อมากขึ้น
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198
ในการติดตั้ง OpenJDK ของฉัน FTR: นั่นเป็นความจริงถ้าฉันลบfinal
ตัวปรับแต่ง Btw. ฉันต้องสร้างterminate
สนาม volatile
มิฉะนั้นการทดสอบจะไม่เสร็จสิ้น
VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170
6: ไม่เพียง แต่ OpenJDK บนโน้ตบุ๊กของฉันเท่านั้นที่สามารถทำซ้ำได้มากขึ้น 40 เท่า แต่การทดสอบแบบคงที่ยังมีปริมาณงานน้อยลงประมาณ 30% นี่อาจเป็นปรากฏการณ์เฉพาะของ ART เพราะฉันได้รับผลลัพธ์ที่คาดหวังบนแท็บเล็ต Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
การโทรแบบคงที่ไม่สามารถแทนที่ได้ (ดังนั้นจึงเป็นตัวเลือกสำหรับอินไลน์เสมอ) และไม่จำเป็นต้องมีการตรวจสอบความว่างเปล่า HotSpot ทำการเพิ่มประสิทธิภาพที่ยอดเยี่ยมมากมายสำหรับวิธีการอินสแตนซ์ซึ่งอาจลบล้างข้อดีเหล่านี้ได้เป็นอย่างดี แต่ก็เป็นสาเหตุที่เป็นไปได้ว่าทำไมการโทรแบบคงที่อาจเร็วกว่า
อย่างไรก็ตามสิ่งนี้ไม่ควรส่งผลกระทบต่อการออกแบบของคุณ - โค้ดในรูปแบบที่อ่านง่ายและเป็นธรรมชาติที่สุด - และต้องกังวลเกี่ยวกับการเพิ่มประสิทธิภาพไมโครประเภทนี้หากคุณมีสาเหตุเพียงอย่างเดียว (ซึ่งคุณแทบจะไม่เคยทำเลย )
เป็นคอมไพเลอร์ / VM เฉพาะ
ดังนั้นจึงอาจไม่คุ้มค่าที่จะรบกวนเว้นแต่คุณจะระบุว่านี่เป็นปัญหาด้านประสิทธิภาพที่สำคัญอย่างแท้จริงในแอปพลิเคชันของคุณ การเพิ่มประสิทธิภาพก่อนวัยเป็นรากเหง้าของความชั่วร้ายทั้งหมด ฯลฯ ...
อย่างไรก็ตามฉันได้เห็นว่าการเพิ่มประสิทธิภาพนี้ทำให้ประสิทธิภาพเพิ่มขึ้นอย่างมากในสถานการณ์ต่อไปนี้:
หากข้อมูลข้างต้นตรงกับคุณการทดสอบอาจคุ้มค่า
นอกจากนี้ยังมีอีกหนึ่งสิ่งที่ดี (และอาจสำคัญกว่าด้วยซ้ำ!) เหตุผลในการใช้วิธีการแบบคงที่ - หากวิธีการนั้นมีความหมายแบบคงที่จริง ๆ (กล่าวคือเหตุผลไม่ได้เชื่อมต่อกับอินสแตนซ์ของคลาสที่กำหนด) มันก็สมเหตุสมผลที่จะทำให้มันคงที่ เพื่อสะท้อนข้อเท็จจริงนี้ โปรแกรมเมอร์ Java ที่มีประสบการณ์จะสังเกตเห็นตัวปรับแต่งแบบคงที่และทันทีที่คิดว่า "aha! วิธีนี้เป็นแบบคงที่ดังนั้นจึงไม่จำเป็นต้องมีอินสแตนซ์และน่าจะไม่ปรับเปลี่ยนสถานะเฉพาะของอินสแตนซ์" ดังนั้นคุณจะสื่อสารลักษณะคงที่ของวิธีการได้อย่างมีประสิทธิภาพ ....
ไม่น่าเชื่ออย่างไม่น่าเชื่อว่าประสิทธิภาพของการโทรแบบคงที่และไม่คงที่จะสร้างความแตกต่างในแอปพลิเคชันของคุณ โปรดจำไว้ว่า "การเพิ่มประสิทธิภาพก่อนเวลาอันควรเป็นรากเหง้าของความชั่วร้ายทั้งหมด"
ดังที่ผู้โพสต์ก่อนหน้านี้ได้กล่าวไว้: ดูเหมือนว่าจะเป็นการเพิ่มประสิทธิภาพก่อนเวลาอันควร
อย่างไรก็ตามมีความแตกต่างอย่างหนึ่ง (ส่วนหนึ่งจากข้อเท็จจริงที่ว่าการเรียกใช้แบบไม่คงที่จำเป็นต้องมีการกด callee-object เพิ่มเติมไปยังสแต็กตัวถูกดำเนินการ):
เนื่องจากวิธีการแบบคงที่ไม่สามารถแทนที่ได้จะไม่มีการค้นหาเสมือนใด ๆในรันไทม์สำหรับการเรียกใช้เมธอดแบบคงที่ ซึ่งอาจส่งผลให้เกิดความแตกต่างที่สังเกตได้ภายใต้สถานการณ์บางอย่าง
ความแตกต่างในระดับไบต์รหัสคือที่เรียกวิธีไม่คงที่จะกระทำผ่านINVOKEVIRTUAL
,INVOKEINTERFACE
หรือในขณะที่เรียกวิธีการแบบคงที่จะกระทำผ่านINVOKESPECIAL
INVOKESTATIC
invokespecial
เนื่องจากไม่ใช่เสมือน
7 ปีต่อมา ...
ฉันไม่มีความมั่นใจอย่างมากในผลลัพธ์ที่ Mike Nakis พบเพราะพวกเขาไม่ได้จัดการกับปัญหาทั่วไปบางอย่างที่เกี่ยวข้องกับการเพิ่มประสิทธิภาพ Hotspot ฉันได้ทำการวัดประสิทธิภาพโดยใช้ JMH และพบว่าค่าใช้จ่ายของวิธีอินสแตนซ์อยู่ที่ประมาณ 0.75% บนเครื่องของฉันเทียบกับการโทรแบบคงที่ เนื่องจากค่าใช้จ่ายที่ต่ำฉันคิดว่ายกเว้นในการดำเนินการที่มีความอ่อนไหวต่อเวลาแฝงมากที่สุดจึงไม่ใช่ประเด็นที่น่ากังวลที่สุดในการออกแบบแอปพลิเคชัน ผลสรุปจากเกณฑ์มาตรฐาน JMH ของฉันมีดังนี้
java -jar target/benchmark.jar
# -- snip --
Benchmark Mode Cnt Score Error Units
MyBenchmark.testInstanceMethod thrpt 200 414036562.933 ± 2198178.163 ops/s
MyBenchmark.testStaticMethod thrpt 200 417194553.496 ± 1055872.594 ops/s
คุณสามารถดูรหัสได้ที่นี่ใน Github
https://github.com/nfisher/svsi
เกณฑ์มาตรฐานนั้นค่อนข้างเรียบง่าย แต่มีจุดมุ่งหมายเพื่อลดการกำจัดโค้ดที่ตายแล้วและการพับคงที่ อาจมีการเพิ่มประสิทธิภาพอื่น ๆ ที่ฉันพลาด / มองข้ามไปและผลลัพธ์เหล่านี้อาจแตกต่างกันไปตามรุ่น JVM และ OS
package ca.junctionbox.svsi;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
class InstanceSum {
public int sum(final int a, final int b) {
return a + b;
}
}
class StaticSum {
public static int sum(final int a, final int b) {
return a + b;
}
}
public class MyBenchmark {
private static final InstanceSum impl = new InstanceSum();
@State(Scope.Thread)
public static class Input {
public int a = 1;
public int b = 2;
}
@Benchmark
public void testStaticMethod(Input i, Blackhole blackhole) {
int sum = StaticSum.sum(i.a, i.b);
blackhole.consume(sum);
}
@Benchmark
public void testInstanceMethod(Input i, Blackhole blackhole) {
int sum = impl.sum(i.a, i.b);
blackhole.consume(sum);
}
}
ops/s
สภาพแวดล้อม ART เป็นหลัก (เช่นการใช้หน่วยความจำการลดขนาดไฟล์. oat เป็นต้น) คุณรู้หรือไม่ว่ามีเครื่องมือ / วิธีง่ายๆที่สามารถใช้ในการเปรียบเทียบเมตริกอื่น ๆ เหล่านี้ได้หรือไม่?
สำหรับการตัดสินใจว่าวิธีใดควรเป็นแบบคงที่ด้านประสิทธิภาพไม่ควรเกี่ยวข้อง หากคุณมีปัญหาด้านประสิทธิภาพการสร้างเมธอดจำนวนมากแบบคงที่จะไม่ช่วยวันนี้ กล่าวได้ว่าวิธีการแบบคงที่แทบจะไม่ช้ากว่าวิธีการอินสแตนซ์ใด ๆ ในกรณีส่วนใหญ่เร็วกว่าเล็กน้อย :
1. ) วิธีการแบบคงที่ไม่ใช่ความหลากหลายดังนั้น JVM จึงมีการตัดสินใจน้อยกว่าในการค้นหารหัสจริงเพื่อดำเนินการ นี่เป็นจุดที่สงสัยใน Age of Hotspot เนื่องจาก Hotspot จะเพิ่มประสิทธิภาพการเรียกวิธีการอินสแตนซ์ที่มีไซต์การใช้งานเพียงไซต์เดียวดังนั้นจึงจะดำเนินการเหมือนกัน
2. ) ความแตกต่างที่ลึกซึ้งอีกประการหนึ่งคือวิธีการแบบคงที่เห็นได้ชัดว่าไม่มีการอ้างอิง "นี้" สิ่งนี้ส่งผลให้สแต็กเฟรมหนึ่งสล็อตมีขนาดเล็กกว่าของเมธอดอินสแตนซ์ที่มีลายเซ็นและเนื้อความเดียวกัน ("this" ถูกใส่ในสล็อต 0 ของตัวแปรโลคัลในระดับ bytecode ในขณะที่วิธีการคงที่สล็อต 0 จะใช้สำหรับช่องแรก พารามิเตอร์ของวิธีการ)
อาจมีความแตกต่างและอาจไปทางใดทางหนึ่งสำหรับโค้ดชิ้นใดชิ้นหนึ่งและอาจมีการเปลี่ยนแปลงแม้กระทั่งรุ่นย่อยของ JVM
นี้เป็นส่วนใหญ่แน่นอนส่วนหนึ่งของ97% ของประสิทธิภาพการขนาดเล็กที่คุณควรลืมเกี่ยวกับ
TableView
บันทึกหลายล้านรายการ
ในทางทฤษฎีราคาไม่แพง
การเริ่มต้นแบบคงที่จะทำได้แม้ว่าคุณจะสร้างอินสแตนซ์ของวัตถุในขณะที่วิธีการแบบคงที่จะไม่ทำการเริ่มต้นใด ๆ ตามปกติในตัวสร้าง
อย่างไรก็ตามฉันยังไม่ได้ทดสอบ
ดังที่ Jon ตั้งข้อสังเกตว่าวิธีการแบบคงที่ไม่สามารถแทนที่ได้ดังนั้นการเรียกใช้วิธีการแบบคงที่อาจเป็น - บนรันไทม์ Java ที่ไร้เดียงสาเพียงพอ - เร็วกว่าการเรียกใช้วิธีอินสแตนซ์
แต่ถึงแม้จะสมมติว่าคุณอยู่ในจุดที่คุณสนใจที่จะทำให้งานออกแบบของคุณยุ่งเหยิงเพื่อประหยัดเวลาเพียงไม่กี่นาโนวินาทีนั่นก็ทำให้เกิดคำถามขึ้นมาอีก: คุณต้องการวิธีการที่จะเอาชนะตัวเองหรือไม่? หากคุณเปลี่ยนรหัสของคุณเพื่อสร้างวิธีการอินสแตนซ์ให้เป็นวิธีการแบบคงที่เพื่อประหยัดหนึ่งนาโนวินาทีที่นี่และที่นั่นจากนั้นหันกลับมาและติดตั้งโปรแกรมมอบหมายงานของคุณเองเหนือสิ่งนั้นของคุณเกือบจะมีประสิทธิภาพน้อยกว่าที่สร้าง ลงในรันไทม์ Java ของคุณแล้ว
ฉันต้องการเพิ่มคำตอบที่ดีอื่น ๆ ที่นี่ซึ่งขึ้นอยู่กับขั้นตอนของคุณเช่น:
Public class MyDao {
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, new MyRowMapper());
};
};
ให้ความสนใจว่าคุณสร้างวัตถุ MyRowMapper ใหม่ต่อการโทรแต่ละครั้ง
ฉันขอแนะนำให้ใช้ที่นี่เป็นเขตข้อมูลคงที่
Public class MyDao {
private static RowMapper myRowMapper = new MyRowMapper();
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, myRowMapper);
};
};