2013-09-25 41 views
18

Có sự khác biệt về hiệu năng giữa nàyChi phí đồng bộ hóa gọi phương thức được đồng bộ hóa từ phương thức được đồng bộ hóa là gì?

synchronized void x() { 
    y(); 
} 

synchronized void y() { 
} 

và điều này

synchronized void x() { 
    y(); 
} 

void y() { 
} 
+1

Tôi sẽ ngạc nhiên nếu có sự khác biệt. Xem thêm http://www.oracle.com/technetwork/java/6-performance-137236.html (2.1.1 và 2.1.2) – assylias

Trả lời

16

Vâng, có một chi phí hiệu suất bổ sung, trừ khi và cho đến khi JVM inlines cuộc gọi đến y(), mà một JIT hiện đại trình biên dịch sẽ làm theo thứ tự khá ngắn. Trước tiên, hãy xem xét trường hợp bạn đã trình bày trong đó y() hiển thị bên ngoài lớp học. Trong trường hợp này, JVM phải kiểm tra nhập y() để đảm bảo rằng nó có thể nhập màn hình vào đối tượng; kiểm tra này sẽ luôn thành công khi cuộc gọi đến từ x(), nhưng không thể bỏ qua, bởi vì cuộc gọi có thể đến từ một khách hàng bên ngoài lớp học. Séc bổ sung này phát sinh một chi phí nhỏ.

Ngoài ra, hãy xem xét trường hợp trong đó y()private. Trong trường hợp này, trình biên dịch vẫn không tối ưu hóa việc đồng bộ hóa; thấy tháo dỡ sau của một sản phẩm nào y():

private synchronized void y(); 
    flags: ACC_PRIVATE, ACC_SYNCHRONIZED 
    Code: 
    stack=0, locals=1, args_size=1 
     0: return 

According to the spec's definition of synchronized, mỗi lối vào thành một khối synchronized hoặc phương pháp thực hiện khóa hành động trên đối tượng, và để lại thực hiện một hành động unlock. Không có chủ đề nào khác có thể có được màn hình của đối tượng đó cho đến khi bộ đếm khóa giảm xuống 0. Có lẽ một số loại phân tích tĩnh có thể chứng minh rằng phương pháp private synchronized chỉ được gọi từ bên trong các phương thức synchronized khác, nhưng hỗ trợ nhiều tệp nguồn của Java sẽ làm cho mong manh đó trở nên mỏng manh nhất, thậm chí bỏ qua sự phản chiếu. This means that the JVM must still increment the counter on entering y():

mục Monitor trên gọi của một synchronized phương pháp, và thoát màn hình trên tuyến trở về, được xử lý ngầm bằng phương pháp gọi Java Virtual Machine và trở lại hướng dẫn, như thể monitorentermonitorexit được sử dụng.

@AmolSonawane correctly notes rằng JVM có thể tối ưu hóa mã này trong thời gian chạy bằng cách thực hiện khóa coarsening, về cơ bản nội tuyến phương pháp y(). Trong trường hợp này, sau khi JVM quyết định thực hiện tối ưu hóa JIT, hãy gọi từ x() đến y() sẽ không phát sinh thêm bất kỳ chi phí hiệu suất nào, nhưng tất nhiên cuộc gọi trực tiếp đến y() từ bất kỳ vị trí nào khác sẽ vẫn cần phải có được màn hình riêng.

+4

Tại sao không chỉ đăng giải thích chi tiết với câu trả lời của bạn? – BLaZuRE

+0

@BLaZuRE Một số câu trả lời của ghế bành không chính xác đã được tăng lên. – chrylis

+0

những gì bạn đang hiển thị không phải là assembly nhưng bytecode - đó không phải là rất phù hợp cho mục đích hiệu suất như JIT sẽ chạy cái gì khác ... Đối với các thông số kỹ thuật, không cố gắng để phản hồi khóa khi 'y' được gọi từ' x' là tuân thủ (vì khóa đã được giữ). – assylias

0

Trong trường hợp cả hai phương pháp được đồng bộ hóa, bạn sẽ khóa màn hình hai lần. Vì vậy, cách tiếp cận đầu tiên sẽ có thêm chi phí của khóa một lần nữa. Nhưng JVM của bạn có thể giảm chi phí khóa bằng cách khóa thô và có thể gọi trực tiếp đến y().

+0

Bạn không cần phải lấy khóa nếu bạn đã giữ nó ... – assylias

+0

Điều này là không đúng, nếu cả hai mehtod được đồng bộ hóa và không tĩnh không có khóa bổ sung bắt buộc. –

+1

"Chủ đề có thể khóa một màn hình cụ thể nhiều lần, mỗi lần mở khóa sẽ đảo ngược tác dụng của một thao tác khóa." - Java Language Specification 17.1 –

2

Tại sao không kiểm tra nó !? Tôi đã chạy một điểm chuẩn nhanh chóng. Phương pháp benchmark() được gọi trong vòng lặp để khởi động. Điều này có thể không siêu chính xác nhưng nó cho thấy một số mô hình thú vị nhất quán.

public class Test { 
    public static void main(String[] args) { 

     for (int i = 0; i < 100; i++) { 
      System.out.println("+++++++++"); 
      benchMark(); 
     } 
    } 

    static void benchMark() { 
     Test t = new Test(); 
     long start = System.nanoTime(); 
     for (int i = 0; i < 100; i++) { 
      t.x(); 
     } 
     System.out.println("Double sync:" + (System.nanoTime() - start)/1e6); 

     start = System.nanoTime(); 
     for (int i = 0; i < 100; i++) { 
      t.x1(); 
     } 
     System.out.println("Single sync:" + (System.nanoTime() - start)/1e6); 
    } 
    synchronized void x() { 
     y(); 
    } 
    synchronized void y() { 
    } 
    synchronized void x1() { 
     y1(); 
    } 
    void y1() { 
    } 
} 

Kết quả (trước 10)

+++++++++ 
Double sync:0.021686 
Single sync:0.017861 
+++++++++ 
Double sync:0.021447 
Single sync:0.017929 
+++++++++ 
Double sync:0.021608 
Single sync:0.016563 
+++++++++ 
Double sync:0.022007 
Single sync:0.017681 
+++++++++ 
Double sync:0.021454 
Single sync:0.017684 
+++++++++ 
Double sync:0.020821 
Single sync:0.017776 
+++++++++ 
Double sync:0.021107 
Single sync:0.017662 
+++++++++ 
Double sync:0.020832 
Single sync:0.017982 
+++++++++ 
Double sync:0.021001 
Single sync:0.017615 
+++++++++ 
Double sync:0.042347 
Single sync:0.023859 

Hình như sự thay đổi thứ hai thực sự là hơi nhanh hơn.

+4

Điểm chuẩn vi sai ... Không chứng minh bất cứ điều gì ... – assylias

+1

@assylias Đó là một microbenchmark với một sự khởi động tốt, tạo ra một mẫu khá phù hợp phù hợp với chi phí của ballpark mà bạn mong đợi nếu không có phương pháp trống. Những lời chỉ trích của bạn về nó là gì? – chrylis

+0

@chrylis 10.000 vòng là đủ cho JIT bắt đầu đá vào - Tôi sẽ không gọi nó là một sự khởi động tốt ... – assylias

8

Kết quả a => không có sự khác biệt thống kê micro benchmark run with jmh

Benchmark      Mean  Mean error Units 
c.a.p.SO18996783.syncOnce  21.003  0.091 nsec/op 
c.a.p.SO18996783.syncTwice  20.937  0.108 nsec/op 

.

Nhìn vào hội đồng được tạo ra cho thấy rằng khóa coarsening đã được thực hiện và y_sync đã được inline trong x_sync mặc dù nó được đồng bộ hóa.

kết quả đầy đủ:

Benchmarks: 
# Running: com.assylias.performance.SO18996783.syncOnce 
Iteration 1 (5000ms in 1 thread): 21.049 nsec/op 
Iteration 2 (5000ms in 1 thread): 21.052 nsec/op 
Iteration 3 (5000ms in 1 thread): 20.959 nsec/op 
Iteration 4 (5000ms in 1 thread): 20.977 nsec/op 
Iteration 5 (5000ms in 1 thread): 20.977 nsec/op 

Run result "syncOnce": 21.003 ±(95%) 0.055 ±(99%) 0.091 nsec/op 
Run statistics "syncOnce": min = 20.959, avg = 21.003, max = 21.052, stdev = 0.044 
Run confidence intervals "syncOnce": 95% [20.948, 21.058], 99% [20.912, 21.094] 

Benchmarks: 
com.assylias.performance.SO18996783.syncTwice 
Iteration 1 (5000ms in 1 thread): 21.006 nsec/op 
Iteration 2 (5000ms in 1 thread): 20.954 nsec/op 
Iteration 3 (5000ms in 1 thread): 20.953 nsec/op 
Iteration 4 (5000ms in 1 thread): 20.869 nsec/op 
Iteration 5 (5000ms in 1 thread): 20.903 nsec/op 

Run result "syncTwice": 20.937 ±(95%) 0.065 ±(99%) 0.108 nsec/op 
Run statistics "syncTwice": min = 20.869, avg = 20.937, max = 21.006, stdev = 0.052 
Run confidence intervals "syncTwice": 95% [20.872, 21.002], 99% [20.829, 21.045] 
+0

Hãy nhớ rằng đây có lẽ là lợi dụng xu hướng chủ đề. Có cách nào thực tế để chạy thử nghiệm này với một màn hình được giám sát không? – chrylis

+0

@chrylis Không chắc chắn nó sẽ tạo ra sự khác biệt: không bao giờ tranh chấp khi có được khóa trong 'y()' vì nó đã được giữ. – assylias

+0

Một thử nghiệm tốt hơn sẽ là đồng bộ hóa một lần. đồng bộ 1000 lần. – momomo

0

Không khác biệt sẽ có mặt ở đó. Vì chủ đề chỉ có nội dung để lấy khóa tại x(). Chủ đề mà có được khóa tại x() có thể có được khóa tại y() mà không có bất kỳ tranh chấp (Bởi vì đó chỉ là thread có thể đạt được điểm đó tại một thời điểm cụ thể). Vì vậy, việc đặt đồng bộ hóa ở đó không có hiệu lực.

1

kiểm tra có thể được tìm thấy bên dưới (Bạn phải đoán những gì một số phương pháp làm nhưng không có gì phức tạp):

Nó kiểm tra chúng với 100 bài mỗi và bắt đầu đếm trung bình sau khi 70% trong số họ đã hoàn thành (như hâm lại) .

Nó in ra một lần ở cuối.

public static final class Test { 
     final int      iterations  =  100; 
     final int      jiterations = 1000000; 
     final int      count   = (int) (0.7 * iterations); 
     final AtomicInteger   finishedSingle = new AtomicInteger(iterations); 
     final AtomicInteger   finishedZynced = new AtomicInteger(iterations); 
     final MovingAverage.Cumulative singleCum  = new MovingAverage.Cumulative(); 
     final MovingAverage.Cumulative zyncedCum  = new MovingAverage.Cumulative(); 
     final MovingAverage   singleConv  = new MovingAverage.Converging(0.5); 
     final MovingAverage   zyncedConv  = new MovingAverage.Converging(0.5); 

     // ----------------------------------------------------------- 
     // ----------------------------------------------------------- 
     public static void main(String[] args) { 
       final Test test = new Test(); 

       for (int i = 0; i < test.iterations; i++) { 
         test.benchmark(i); 
       } 

       Threads.sleep(1000000); 
     } 
     // ----------------------------------------------------------- 
     // ----------------------------------------------------------- 

     void benchmark(int i) { 

       Threads.async(()->{ 
         long start = System.nanoTime(); 

         for (int j = 0; j < jiterations; j++) { 
           a(); 
         } 

         long elapsed = System.nanoTime() - start; 
         int v = this.finishedSingle.decrementAndGet(); 
         if (v <= count) { 
           singleCum.add (elapsed); 
           singleConv.add(elapsed); 
         } 

         if (v == 0) { 
           System.out.println(elapsed); 
           System.out.println("Single Cum:\t\t" + singleCum.val()); 
           System.out.println("Single Conv:\t" + singleConv.val()); 
           System.out.println(); 

         } 
       }); 

       Threads.async(()->{ 

         long start = System.nanoTime(); 
         for (int j = 0; j < jiterations; j++) { 
           az(); 
         } 

         long elapsed = System.nanoTime() - start; 

         int v = this.finishedZynced.decrementAndGet(); 
         if (v <= count) { 
           zyncedCum.add(elapsed); 
           zyncedConv.add(elapsed); 
         } 

         if (v == 0) { 
           // Just to avoid the output not overlapping with the one above 
           Threads.sleep(500); 
           System.out.println(); 
           System.out.println("Zynced Cum: \t" + zyncedCum.val()); 
           System.out.println("Zynced Conv:\t" + zyncedConv.val()); 
           System.out.println(); 
         } 
       }); 

     }      

     synchronized void a() { b(); } 
        void b() { c(); } 
        void c() { d(); } 
        void d() { e(); } 
        void e() { f(); } 
        void f() { g(); } 
        void g() { h(); } 
        void h() { i(); } 
        void i() { } 

     synchronized void az() { bz(); } 
     synchronized void bz() { cz(); } 
     synchronized void cz() { dz(); } 
     synchronized void dz() { ez(); } 
     synchronized void ez() { fz(); } 
     synchronized void fz() { gz(); } 
     synchronized void gz() { hz(); } 
     synchronized void hz() { iz(); } 
     synchronized void iz() {} 
} 

MovingAverage.Cumulative add được cơ bản (thực hiện nguyên tử): trung bình = (trung bình * (n) + số)/(++ n);

MovingAverage.Converging bạn có thể tra cứu nhưng sử dụng công thức khác.

Kết quả sau một hâm lại 50 thứ hai:

Với: jiterations -> 1000000

Zynced Cum:  3.2017985649516254E11 
Zynced Conv: 8.11945143126507E10 

Single Cum:  4.747368153507841E11 
Single Conv: 8.277793176290959E10 

Đó là nano giây trung bình. Điều đó thực sự không có gì và thậm chí cho thấy rằng được đồng bộ hóa mất ít thời gian hơn.

Với: jiterations -> gốc * 10 (mất nhiều thời gian hơn)

Zynced Cum:  7.462005651190714E11 
Zynced Conv: 9.03751742946726E11 

Single Cum:  9.088230941676143E11 
Single Conv: 9.09877020004914E11 

Như bạn có thể thấy kết quả cho thấy nó thực sự không phải là một sự khác biệt lớn. Giá trị thực tế được thực hiện có thời gian trung bình là thấp hơn cho thời gian hoàn thành 30% cuối cùng.

Với mỗi chủ đề một (lặp = 1) và jiterations = original * 100;

Zynced Cum:  6.9167088486E10 
Zynced Conv: 6.9167088486E10 

Single Cum:  6.9814404337E10 
Single Conv: 6.9814404337E10 

Trong cùng chủ đề môi trường (loại bỏ Threads.async gọi)

Với: jiterations -> gốc * 10

Single Cum:  2.940499529542545E8 
Single Conv: 5.0342450600964054E7 


Zynced Cum:  1.1930525617915475E9 
Zynced Conv: 6.672312498662484E8 

Một zynced đây có vẻ là chậm . Theo thứ tự ~ 10. Lý do cho điều này có thể là do một người chạy đua sau mỗi lần, ai biết được. Không có năng lượng để thử ngược lại.

chạy thử cuối với:

public static final class Test { 
     final int      iterations  =  100; 
     final int      jiterations = 10000000; 
     final int      count   = (int) (0.7 * iterations); 
     final AtomicInteger   finishedSingle = new AtomicInteger(iterations); 
     final AtomicInteger   finishedZynced = new AtomicInteger(iterations); 
     final MovingAverage.Cumulative singleCum  = new MovingAverage.Cumulative(); 
     final MovingAverage.Cumulative zyncedCum  = new MovingAverage.Cumulative(); 
     final MovingAverage   singleConv  = new MovingAverage.Converging(0.5); 
     final MovingAverage   zyncedConv  = new MovingAverage.Converging(0.5); 

     // ----------------------------------------------------------- 
     // ----------------------------------------------------------- 
     public static void main(String[] args) { 
       final Test test = new Test(); 

       for (int i = 0; i < test.iterations; i++) { 
         test.benchmark(i); 
       } 

       Threads.sleep(1000000); 
     } 
     // ----------------------------------------------------------- 
     // ----------------------------------------------------------- 

     void benchmark(int i) { 

         long start = System.nanoTime(); 

         for (int j = 0; j < jiterations; j++) { 
           a(); 
         } 

         long elapsed = System.nanoTime() - start; 
         int s = this.finishedSingle.decrementAndGet(); 
         if (s <= count) { 
           singleCum.add (elapsed); 
           singleConv.add(elapsed); 
         } 

         if (s == 0) { 
           System.out.println(elapsed); 
           System.out.println("Single Cum:\t\t" + singleCum.val()); 
           System.out.println("Single Conv:\t" + singleConv.val()); 
           System.out.println(); 

         } 


         long zstart = System.nanoTime(); 
         for (int j = 0; j < jiterations; j++) { 
           az(); 
         } 

         long elapzed = System.nanoTime() - zstart; 

         int z = this.finishedZynced.decrementAndGet(); 
         if (z <= count) { 
           zyncedCum.add(elapzed); 
           zyncedConv.add(elapzed); 
         } 

         if (z == 0) { 
           // Just to avoid the output not overlapping with the one above 
           Threads.sleep(500); 
           System.out.println(); 
           System.out.println("Zynced Cum: \t" + zyncedCum.val()); 
           System.out.println("Zynced Conv:\t" + zyncedConv.val()); 
           System.out.println(); 
         } 

     }      

     synchronized void a() { b(); } 
        void b() { c(); } 
        void c() { d(); } 
        void d() { e(); } 
        void e() { f(); } 
        void f() { g(); } 
        void g() { h(); } 
        void h() { i(); } 
        void i() { } 

     synchronized void az() { bz(); } 
     synchronized void bz() { cz(); } 
     synchronized void cz() { dz(); } 
     synchronized void dz() { ez(); } 
     synchronized void ez() { fz(); } 
     synchronized void fz() { gz(); } 
     synchronized void gz() { hz(); } 
     synchronized void hz() { iz(); } 
     synchronized void iz() {} 
} 

Kết luận, có thực sự là không có sự khác biệt.

Các vấn đề liên quan