Apakah panggilan statis Java lebih mahal atau lebih murah daripada panggilan non-statis?

95

Apakah ada manfaat kinerja dengan satu atau lain cara? Apakah compiler / VM spesifik? Saya menggunakan Hotspot.

Andy Faibishenko
sumber

Jawaban:

74

Pertama: Anda tidak boleh membuat pilihan statis vs non-statis berdasarkan kinerja.

Kedua: dalam praktiknya, tidak ada bedanya. Hotspot dapat memilih untuk mengoptimalkan dengan cara yang membuat panggilan statis lebih cepat untuk satu metode, panggilan non-statis lebih cepat untuk metode lainnya.

Ketiga: banyak mitos seputar statis versus non-statis didasarkan pada JVM yang sangat lama (yang tidak mendekati pengoptimalan seperti yang dilakukan Hotspot), atau beberapa hal sepele yang diingat tentang C ++ (di mana panggilan dinamis menggunakan satu lagi akses memori daripada panggilan statis).

Segera
sumber
1
Anda benar, Anda seharusnya tidak memilih metode statis berdasarkan ini saja. Namun, jika metode statis sesuai dengan desain dengan baik, penting untuk diketahui bahwa metode tersebut setidaknya sama cepatnya, jika tidak lebih cepat dari metode instans dan tidak boleh dikesampingkan berdasarkan kinerja.
Akankah
2
@AaronDigulla -.- bagaimana jika saya memberi tahu Anda bahwa saya datang ke sini karena saya sedang mengoptimalkan sekarang, bukan sebelum waktunya, tetapi ketika saya benar-benar membutuhkannya? Anda berasumsi bahwa OP ingin mengoptimalkan sebelum waktunya, tetapi Anda tahu bahwa situs ini agak global ... bukan? Saya tidak ingin menjadi kasar, tapi tolong jangan berasumsi hal-hal seperti ini lain kali.
Dalibor Filus
1
@DaliborFilus Saya perlu menemukan keseimbangan. Menggunakan metode statis menyebabkan semua jenis masalah, jadi sebaiknya dihindari, terutama saat Anda tidak tahu apa yang Anda lakukan. Kedua, sebagian besar kode "lambat" adalah karena desain (buruk), bukan karena Bahasa Pilihan lambat. Jika kode Anda lambat, metode statis mungkin tidak akan menyimpannya kecuali metode pemanggilannya tidak melakukan apa-apa . Dalam kebanyakan kasus, kode dalam metode akan mengecilkan overhead panggilan.
Aaron Digulla
6
Suara negatif. Ini tidak menjawab pertanyaan itu. Pertanyaan yang diajukan tentang manfaat kinerja. Itu tidak meminta pendapat tentang prinsip desain.
Colm Bhandal
4
Jika saya melatih burung beo untuk mengatakan "opimasi prematur adalah akar dari semua kejahatan", saya akan mendapatkan 1000 suara dari orang-orang yang tahu banyak tentang penampilan seperti burung beo.
rghome
62

Empat tahun kemudian...

Oke, dengan harapan dapat menjawab pertanyaan ini sekali dan selamanya, saya telah menulis sebuah patokan yang menunjukkan bagaimana berbagai jenis panggilan (virtual, non-virtual, statis) dibandingkan satu sama lain.

Saya menjalankannya dengan ideone , dan inilah yang saya dapatkan:

(Jumlah iterasi yang lebih besar lebih baik.)

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.

Seperti yang diharapkan, panggilan metode virtual adalah yang paling lambat, panggilan metode non-virtual lebih cepat, dan panggilan metode statis bahkan lebih cepat.

Apa yang tidak saya harapkan adalah perbedaannya menjadi begitu jelas: Panggilan metode virtual diukur untuk berjalan kurang dari setengah kecepatan panggilan metode non-virtual, yang pada gilirannya diukur untuk menjalankan keseluruhan 15% lebih lambat daripada panggilan statis. Itulah yang ditunjukkan oleh pengukuran ini; perbedaan aktual sebenarnya harus sedikit lebih jelas, karena untuk setiap panggilan metode virtual, nonvirtual, dan statis, kode pembandingan saya memiliki overhead konstan tambahan untuk menambah satu variabel integer, memeriksa variabel boolean, dan mengulang jika tidak benar.

Saya kira hasilnya akan bervariasi dari CPU ke CPU, dan dari JVM ke JVM, jadi cobalah dan lihat apa yang Anda dapatkan:

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) {}
    } );
}

Perlu dicatat bahwa perbedaan kinerja ini hanya berlaku untuk kode yang tidak melakukan apa pun selain menjalankan metode tanpa parameter. Kode lain apa pun yang Anda miliki di antara pemanggilan akan mengurangi perbedaan, dan ini termasuk penerusan parameter. Sebenarnya, perbedaan 15% antara panggilan statis dan nonvirtual mungkin dijelaskan secara lengkap oleh fakta bahwa thispointer tidak harus diteruskan ke metode statis. Jadi, hanya perlu sedikit kode yang melakukan hal-hal sepele di antara panggilan untuk perbedaan antara berbagai jenis panggilan yang akan diencerkan hingga tidak memiliki dampak bersih sama sekali.

Juga, panggilan metode virtual ada karena suatu alasan; mereka memang memiliki tujuan untuk melayani, dan mereka diimplementasikan menggunakan cara paling efisien yang disediakan oleh perangkat keras yang mendasarinya. (Set instruksi CPU.) Jika, dalam keinginan Anda untuk menghilangkannya dengan menggantinya dengan panggilan nonvirtual atau statis, Anda akhirnya harus menambahkan sebanyak sedikit kode tambahan untuk meniru fungsinya, maka overhead bersih yang Anda hasilkan terikat menjadi tidak kurang, tetapi lebih. Sangat mungkin, sangat banyak, sangat banyak.

Mike Nakis
sumber
7
'Virtual' adalah istilah C ++. Tidak ada metode virtual di Java. Ada metode biasa, yaitu runtime-polymorphic, dan metode statis atau final, yang tidak.
Zhenya
16
@levgen ya, untuk seseorang yang sudut pandangnya sesempit gambaran umum tingkat tinggi resmi dari bahasa tersebut, persis seperti yang Anda katakan. Tetapi tentu saja konsep tingkat tinggi diimplementasikan dengan menggunakan mekanisme tingkat rendah yang mapan yang telah ditemukan lama sebelum java muncul, dan metode virtual adalah salah satunya. Jika Anda hanya melihat sekilas di
balik terpal
13
Terima kasih telah menjawab pertanyaan tanpa membuat praduga tentang pengoptimalan prematur. Jawaban yang bagus.
vegemite4me
3
Ya, itulah yang saya maksud. Pokoknya saya baru saja menjalankan tes pada mesin saya. Selain jitter yang dapat Anda harapkan untuk benchmark seperti itu, tidak ada perbedaan dalam kecepatan apa pun: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198pada instalasi OpenJDK saya. FTR: Itu bahkan benar jika saya menghapus finalpengubahnya. Btw. Saya harus membuat terminatelapangan volatile, jika tidak tes tidak selesai.
Marten
4
FYI, saya mendapatkan hasil yang agak mengejutkan pada Nexus 5 yang menjalankan Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Tidak hanya OpenJDK di notebook saya yang berhasil melakukan 40x lebih banyak iterasi, pengujian statis selalu memiliki throughput sekitar 30% lebih sedikit. Ini mungkin fenomena khusus ART, karena saya mendapatkan hasil yang diharapkan pada tablet Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten
46

Nah, panggilan statis tidak dapat diganti (jadi selalu kandidat untuk sebaris), dan tidak memerlukan pemeriksaan nullity apa pun. HotSpot melakukan banyak pengoptimalan keren untuk metode contoh yang mungkin meniadakan keuntungan ini, tetapi itu mungkin alasan mengapa panggilan statis bisa lebih cepat.

Namun, itu seharusnya tidak memengaruhi desain Anda - kode dengan cara yang paling mudah dibaca dan alami - dan hanya khawatir tentang pengoptimalan mikro semacam ini jika Anda memiliki penyebabnya (yang hampir tidak akan pernah Anda lakukan ).

Jon Skeet
sumber
Itu kemungkinan alasan mengapa panggilan statis bisa lebih cepat. Bisakah Anda menjelaskan alasannya kepada saya?
JavaTechnical
6
@JavaTechnical: Jawabannya menjelaskan alasan tersebut - tidak ada penggantian (yang berarti Anda tidak perlu mengerjakan implementasi untuk digunakan setiap saat dan Anda bisa sebaris) dan Anda tidak perlu memeriksa apakah Anda memanggil metode pada referensi null.
Jon Skeet
6
@JavaTechnical: Saya tidak mengerti. Saya baru saja memberi Anda hal-hal yang tidak perlu dihitung / diperiksa untuk metode statis, bersama dengan peluang sebaris. Tidak melakukan pekerjaan adalah keuntungan kinerja. Apa yang tersisa untuk dipahami?
Jon Skeet
Apakah variabel statis diambil lebih cepat daripada variabel non-statis?
JavaTechnical
1
@JavaTechnical: Tidak ada pemeriksaan nullity yang harus dilakukan - tetapi jika kompiler JIT dapat menghapus pemeriksaan itu (yang akan menjadi konteks khusus), saya tidak akan mengharapkan banyak perbedaan. Hal-hal seperti apakah memori dalam cache akan jauh lebih penting.
Jon Skeet
18

Ini adalah compiler / VM tertentu.

  • Secara teori , panggilan statis dapat dibuat sedikit lebih efisien karena tidak perlu melakukan pencarian fungsi virtual, dan juga dapat menghindari overhead parameter "ini" yang tersembunyi.
  • Dalam praktiknya , banyak kompiler yang akan mengoptimalkannya.

Oleh karena itu, mungkin tidak perlu dirisaukan kecuali Anda telah mengidentifikasi ini sebagai masalah kinerja yang benar-benar kritis dalam aplikasi Anda. Pengoptimalan dini adalah akar dari semua kejahatan, dll.

Namun saya telah melihat pengoptimalan ini memberikan peningkatan kinerja yang substansial dalam situasi berikut:

  • Metode melakukan perhitungan matematis yang sangat sederhana tanpa akses memori
  • Metode dipanggil jutaan kali per detik dalam lingkaran dalam yang rapat
  • Aplikasi terikat CPU di mana setiap bit kinerja penting

Jika hal di atas berlaku untuk Anda, mungkin perlu diuji.

Ada juga satu alasan bagus lainnya (dan berpotensi bahkan lebih penting!) Untuk menggunakan metode statis - jika metode tersebut benar-benar memiliki semantik statis (yaitu secara logis tidak terhubung ke instance kelas tertentu) maka masuk akal untuk membuatnya statis untuk mencerminkan fakta ini. Pemrogram Java yang berpengalaman kemudian akan melihat pengubah statis dan langsung berpikir "aha! Metode ini statis sehingga tidak memerlukan instance dan mungkin tidak memanipulasi status instance tertentu". Jadi Anda akan mengkomunikasikan sifat statis dari metode ini secara efektif ....

mikera
sumber
14

Seperti yang dikatakan poster sebelumnya: Ini sepertinya pengoptimalan yang prematur.

Namun, ada satu perbedaan (bagian dari fakta bahwa pemanggilan non-statis memerlukan dorongan tambahan dari objek callee ke tumpukan operan):

Karena metode statis tidak dapat diganti, tidak akan ada pencarian virtual dalam waktu proses untuk panggilan metode statis. Ini dapat menghasilkan perbedaan yang dapat diamati dalam beberapa keadaan.

Perbedaan pada tingkat kode byte adalah bahwa pemanggilan metode non-statis dilakukan melalui INVOKEVIRTUAL, INVOKEINTERFACEatau INVOKESPECIALsementara pemanggilan metode statis dilakukan melalui INVOKESTATIC.

aioobe
sumber
2
Namun, metode instance privat (setidaknya biasanya) dipanggil menggunakan invokespecialkarena bukan virtual.
Mark Peters
Ah, menarik, saya hanya bisa memikirkan konstruktor, itu sebabnya saya menghilangkannya! Terima kasih! (memperbarui jawaban)
aioobe
2
JVM akan dioptimalkan jika ada satu jenis yang dipakai. Jika B meluas A, dan tidak ada instance B yang dibuat, panggilan metode pada A tidak memerlukan pencarian tabel virtual.
Steve Kuo
13

Sangat tidak mungkin bahwa perbedaan apa pun dalam kinerja panggilan statis versus panggilan non-statis membuat perbedaan dalam aplikasi Anda. Ingatlah bahwa "pengoptimalan prematur adalah akar dari segala kejahatan".

DJClayworth
sumber
bisakah Anda menjelaskan lebih lanjut apa "" pengoptimalan dini adalah akar dari segala kejahatan "."?
pengguna2121
Pertanyaannya adalah "Apakah ada manfaat kinerja dengan satu atau lain cara?", Dan ini menjawab pertanyaan itu dengan tepat.
DJClayworth
13

7 tahun kemudian ...

Saya tidak terlalu yakin dengan hasil yang ditemukan Mike Nakis karena hasil tersebut tidak membahas beberapa masalah umum yang berkaitan dengan pengoptimalan Hotspot. Saya telah menginstrumentasi tolok ukur menggunakan JMH dan menemukan overhead metode instans menjadi sekitar 0,75% di mesin saya vs panggilan statis. Mengingat overhead yang rendah, saya pikir kecuali dalam operasi yang paling sensitif latensi, ini bisa dibilang bukan perhatian terbesar dalam desain aplikasi. Hasil ringkasan dari benchmark JMH saya adalah sebagai berikut;

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

Anda dapat melihat kode di sini di Github;

https://github.com/nfisher/svsi

Tolok ukur itu sendiri cukup sederhana tetapi bertujuan untuk meminimalkan penghapusan kode mati dan pelipatan konstan. Mungkin ada optimisasi lain yang saya lewatkan / abaikan dan hasil ini cenderung bervariasi per rilis JVM dan 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);
    }
}
Nathan
sumber
1
Minat akademis murni di sini. Saya ingin tahu tentang manfaat potensial yang mungkin dimiliki pengoptimalan mikro semacam ini pada metrik selain ops/sterutama di lingkungan ART (misalnya penggunaan memori, pengurangan ukuran file .oat, dll). Apakah Anda mengetahui alat / cara yang relatif sederhana yang dapat dicoba untuk mengukur metrik lain ini?
Ryan Thomas
Angka hotspot di luar sana tidak ada ekstensi ke InstanceSum di classpath. Coba tambahkan kelas lain yang memperluas InstanceSum dan mengganti metode.
milan
12

Untuk keputusan jika suatu metode harus statis, aspek kinerja harus tidak relevan. Jika Anda memiliki masalah kinerja, membuat banyak metode statis tidak akan menghemat waktu. Meskipun demikian, metode statis hampir pasti tidak lebih lambat daripada metode instance mana pun, dalam banyak kasus sedikit lebih cepat :

1.) Metode statis tidak polimorfik, sehingga JVM memiliki lebih sedikit keputusan yang harus dibuat untuk menemukan kode yang sebenarnya untuk dieksekusi. Ini adalah titik diperdebatkan di Age of Hotspot, karena Hotspot akan mengoptimalkan panggilan metode instance yang hanya memiliki satu situs implementasi, sehingga mereka akan melakukan hal yang sama.

2.) Perbedaan halus lainnya adalah bahwa metode statis jelas tidak memiliki referensi "ini". Ini menghasilkan bingkai tumpukan satu slot lebih kecil daripada metode instance dengan tanda tangan dan isi yang sama ("ini" diletakkan di slot 0 dari variabel lokal pada tingkat bytecode, sedangkan untuk metode statis slot 0 digunakan untuk yang pertama parameter metode).

Durandal
sumber
5

Mungkin ada perbedaan, dan mungkin berlaku untuk bagian kode tertentu, dan mungkin berubah bahkan dengan rilis kecil JVM.

Ini pasti bagian dari 97% efisiensi kecil yang harus Anda lupakan .

Michael Borgwardt
sumber
2
Salah. Anda tidak bisa berasumsi apapun. Ini bisa menjadi loop ketat yang diperlukan untuk UI front-end yang dapat membuat perbedaan besar tentang seberapa "tajam" UI tersebut. Misalnya, mencari TableViewjutaan catatan.
trilogi
0

Secara teori, lebih murah.

Inisialisasi statis akan dilakukan meskipun Anda membuat instance objek, sedangkan metode statis tidak akan melakukan inisialisasi apa pun yang biasanya dilakukan dalam konstruktor.

Namun, saya belum menguji ini.

Powerlord
sumber
1
@R. Bemrose, apa hubungan inisialisasi statis dengan pertanyaan ini?
Kirk Woll
@Kirk Woll: Karena Inisialisasi Statis dilakukan pertama kali kelas direferensikan ... termasuk sebelum pemanggilan metode statis pertama.
Powerlord
@R. Bemrose, tentu saja, saat memuat kelas ke dalam VM untuk memulai. Sepertinya non-sequitor, IMO.
Kirk Woll
0

Seperti yang dicatat Jon, metode statis tidak dapat diganti, jadi dengan hanya memanggil metode statis mungkin - pada runtime Java yang cukup naif - lebih cepat daripada memanggil metode instance.

Tapi kemudian, bahkan dengan asumsi Anda berada pada titik di mana Anda peduli untuk mengacaukan desain Anda untuk menghemat beberapa nanodetik, itu hanya memunculkan pertanyaan lain: apakah Anda memerlukan metode untuk mengganti diri Anda sendiri? Jika Anda mengubah kode Anda untuk membuat metode contoh menjadi metode statis untuk menyimpan nanodetik di sana-sini, dan kemudian berbalik dan menerapkan dispatcher Anda sendiri di atas itu, Anda hampir pasti akan kurang efisien daripada yang dibangun ke dalam runtime Java Anda.

Ken
sumber
-2

Saya ingin menambahkan jawaban hebat lainnya di sini yang juga bergantung pada aliran Anda, misalnya:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new MyRowMapper());
   };
};

Perhatikan bahwa Anda membuat objek MyRowMapper baru untuk setiap panggilan.
Sebagai gantinya, saya menyarankan untuk menggunakan bidang statis di sini.

Public class MyDao {

   private static RowMapper myRowMapper = new MyRowMapper();
   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
Yair Zaslavsky
sumber