Hiệu suất của biến ThreadLocal


86

Đọc từ ThreadLocalbiến chậm hơn bao nhiêu so với từ trường thông thường?

Cụ thể hơn là việc tạo đối tượng đơn giản nhanh hơn hay chậm hơn việc truy cập vào ThreadLocalbiến?

Tôi giả sử rằng nó đủ nhanh để ThreadLocal<MessageDigest>việc tạo phiên bản nhanh hơn nhiều sau đó tạo phiên bản MessageDigestmọi lúc. Nhưng điều đó có áp dụng cho byte [10] hoặc byte [1000] không?

Chỉnh sửa: Câu hỏi là điều gì đang thực sự xảy ra khi gọi ThreadLocal's get? Nếu đó chỉ là một lĩnh vực, giống như bất kỳ lĩnh vực nào khác, thì câu trả lời sẽ là "nó luôn luôn nhanh nhất", phải không?


2
Cục bộ luồng về cơ bản là một trường chứa một bản đồ băm và một bản tra cứu trong đó khóa là đối tượng luồng hiện tại. Do đó, nó chậm hơn nhiều nhưng vẫn nhanh. :)
eckes

1
@eckes: nó chắc chắn hoạt động như vậy, nhưng nó thường không được triển khai theo cách này. Thay vào đó, Threads chứa (không đồng bộ) hashmap nơi quan trọng là hiện tại ThreadLocalđối tượng
sbk

Câu trả lời:


40

Chạy các điểm chuẩn chưa xuất bản, ThreadLocal.getmất khoảng 35 chu kỳ mỗi lần lặp trên máy của tôi. Không phải là một vấn đề lớn. Trong triển khai của Sun, một bản đồ băm thăm dò tuyến tính tùy chỉnh trong các Threadbản đồ từ ThreadLocalcác giá trị. Bởi vì nó chỉ được truy cập bởi một luồng duy nhất, nó có thể rất nhanh.

Việc phân bổ các đối tượng nhỏ mất một số chu kỳ tương tự, mặc dù do cạn bộ nhớ cache, bạn có thể nhận được số liệu thấp hơn trong một vòng lặp chặt chẽ.

Việc xây dựng MessageDigestcó thể tương đối tốn kém. Nó có một số lượng trạng thái hợp lý và việc xây dựng thông qua Providercơ chế SPI. Ví dụ: bạn có thể tối ưu hóa bằng cách sao chép hoặc cung cấp Provider.

Chỉ vì bộ nhớ cache trong một ThreadLocalthay vì tạo có thể nhanh hơn không có nghĩa là hiệu suất hệ thống sẽ tăng lên. Bạn sẽ có thêm chi phí liên quan đến GC làm chậm mọi thứ.

Trừ khi ứng dụng của bạn sử dụng rất nhiều, MessageDigestbạn có thể muốn xem xét sử dụng bộ đệm an toàn luồng thông thường để thay thế.


5
IMHO, cách nhanh nhất là bỏ qua SPI và sử dụng một cái gì đó như thế new org.bouncycastle.crypto.digests.SHA1Digest(). Tôi khá chắc chắn rằng không có bộ nhớ cache nào có thể đánh bại nó.
maaartinus

57

Vào năm 2009, một số JVM đã triển khai ThreadLocal bằng cách sử dụng HashMap không được đồng bộ hóa trong đối tượng Thread.currentThread (). Điều này làm cho nó cực kỳ nhanh (tất nhiên là không nhanh bằng cách sử dụng truy cập trường thông thường), cũng như đảm bảo rằng đối tượng ThreadLocal được ngăn nắp khi Thread chết. Cập nhật câu trả lời này vào năm 2016, có vẻ như hầu hết (tất cả?) JVM mới hơn sử dụng Bản đồ ThreadLocalMap với tính năng thăm dò tuyến tính. Tôi không chắc chắn về hiệu suất của những thứ đó - nhưng tôi không thể tưởng tượng được rằng nó tệ hơn đáng kể so với việc triển khai trước đó.

Tất nhiên, Object () mới cũng rất nhanh trong những ngày này, và Garbage Collectors cũng rất giỏi trong việc thu hồi các vật thể tồn tại trong thời gian ngắn.

Trừ khi bạn chắc chắn rằng việc tạo đối tượng sẽ tốn kém hoặc bạn cần duy trì một số trạng thái trên cơ sở từng luồng, tốt hơn bạn nên chuyển sang giải pháp cấp phát đơn giản hơn khi cần thiết và chỉ chuyển sang triển khai ThreadLocal khi hồ sơ cho bạn biết rằng bạn cần.


4
+1 vì là câu trả lời duy nhất để thực sự giải quyết câu hỏi.
cletus

Bạn có thể cho tôi một ví dụ về một JVM hiện đại không sử dụng thăm dò tuyến tính cho ThreadLocalMap không? Java 8 OpenJDK dường như vẫn đang sử dụng ThreadLocalMap với tính năng thăm dò tuyến tính. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick

1
@Karthick Xin lỗi không, tôi không thể. Tôi đã viết điều này trở lại vào năm 2009. Tôi sẽ cập nhật.
Bill Michell,

34

Câu hỏi hay, gần đây tôi đã tự hỏi mình như vậy. Để cung cấp cho bạn những con số xác định, các điểm chuẩn bên dưới (trong Scala, được biên dịch thành các mã byte gần như giống với mã Java tương đương):

var cnt: String = ""
val tlocal = new java.lang.ThreadLocal[String] {
  override def initialValue = ""
}

def loop_heap_write = {                                                                                                                           
  var i = 0                                                                                                                                       
  val until = totalwork / threadnum                                                                                                               
  while (i < until) {                                                                                                                             
    if (cnt ne "") cnt = "!"                                                                                                                      
    i += 1                                                                                                                                        
  }                                                                                                                                               
  cnt                                                                                                                                          
} 

def threadlocal = {
  var i = 0
  val until = totalwork / threadnum
  while (i < until) {
    if (tlocal.get eq null) i = until + i + 1
    i += 1
  }
  if (i > until) println("thread local value was null " + i)
}

có sẵn ở đây , được thực hiện trên AMD 4x 2,8 GHz lõi kép và i7 lõi ​​tứ với siêu phân luồng (2,67 GHz).

Đây là những con số:

i7

Thông số kỹ thuật: Intel i7 2x lõi tứ @ 2,67 GHz Kiểm tra: scala.threads.

Tên kiểm tra: loop_heap_read

Số chủ đề: 1 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 9.0069 9.0036 9.0017 9.0084 9.0074 (trung bình = 9.1034 phút = 8.9986 max = 21.0306)

Số chủ đề: 2 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 4,5563 4,7128 4,5663 4,5617 4,5724 (trung bình = 4,6337 phút = 4,5509 tối đa = 13,9476)

Số chủ đề: 4 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 2,3946 2,3979 2,3934 2,3937 2,3964 (trung bình = 2,5113 phút = 2,3884 tối đa = 13,5496)

Số chủ đề: 8 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 2.4479 2.4362 2.4323 2.4472 2.4383 (trung bình = 2,5562 phút = 2,4166 tối đa = 10,3726)

Tên kiểm tra: threadlocal

Số chủ đề: 1 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 91,1741 90,8978 90,6181 90,6200 90,6113 (trung bình = 91,0291 phút = 90,6000 tối đa = 129,7501)

Số chủ đề: 2 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 45.3838 45.3858 45.6676 45.3772 45.3839 (trung bình = 46.0555 phút = 45.3726 tối đa = 90.7108)

Số chủ đề: 4 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 22.8118 22.8135 59.1753 22.8229 22.8172 (trung bình = 23.9752 phút = 22.7951 tối đa = 59.1753)

Số chủ đề: 8 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 22.2965 22.2415 22.3438 22.3109 22.4460 (trung bình = 23.2676 phút = 22.2346 tối đa = 50.3583)

AMD

Thông số kỹ thuật: AMD 8220 4x lõi kép @ 2,8 GHz Kiểm tra: scala.threads.

Tên kiểm tra: loop_heap_read

Tổng số bài thi: 20000000 Số chủ đề: 1 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 12,625 12,631 12,634 12,632 12,628 (trung bình = 12,7333 phút = 12,619 tối đa = 26,698)

Tên bài kiểm tra: loop_heap_read Tổng công việc: 20000000

Thời gian chạy: (hiển thị 5 lần cuối) 6.412 6.424 6.408 6.397 6.43 (trung bình = 6.5367 phút = 6.393 tối đa = 19.716)

Số chủ đề: 4 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 3,385 4,298 9,7 6,535 3,385 (trung bình = 5,6079 phút = 3,354 tối đa = 21,603)

Số chủ đề: 8 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 5.389 5.795 10.818 3.823 3.824 (trung bình = 5.5810 phút = 2.405 tối đa = 19,755)

Tên kiểm tra: threadlocal

Số chủ đề: 1 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 200.217 207.335 200.241 207.342 200.23 (trung bình = 202,2424 phút = 200.184 tối đa = 245.369)

Số chủ đề: 2 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 100.208 100.199 100.211 103.781 100.215 (trung bình = 102.2238 phút = 100.192 tối đa = 129,505)

Số chủ đề: 4 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 62.101 67.629 62.087 52.021 55.766 (trung bình = 65.6361 phút = 50.282 tối đa = 167.433)

Số chủ đề: 8 Tổng số bài kiểm tra: 200

Thời gian chạy: (hiển thị 5 lần cuối) 40.672 74.301 34.434 41.549 28.119 (trung bình = 54.7701 phút = 28.119 tối đa = 94.424)

Tóm lược

Một luồng cục bộ có giá trị khoảng 10-20 lần so với đọc đống. Nó dường như cũng mở rộng quy mô tốt khi triển khai JVM này và các kiến ​​trúc này với số lượng bộ xử lý.


5
+1 Kudo về việc là người duy nhất đưa ra kết quả định lượng. Tôi hơi nghi ngờ vì những thử nghiệm này là trong Scala, nhưng như bạn đã nói, các mã bytecodes của Java sẽ tương tự ...
Gravity

Cảm ơn! Vòng lặp while này dẫn đến hầu như cùng một mã byte như mã Java tương ứng sẽ tạo ra. Tuy nhiên, các thời điểm khác nhau có thể được quan sát trên các máy ảo khác nhau - điều này đã được thử nghiệm trên Sun JVM1.6.
axel22

Mã điểm chuẩn này không mô phỏng một trường hợp sử dụng tốt cho ThreadLocal. Trong phương thức đầu tiên: mọi luồng sẽ có một đại diện được chia sẻ trong bộ nhớ, chuỗi không thay đổi. Trong phương pháp thứ hai, bạn chuẩn chi phí của tra cứu bảng băm trong đó chuỗi không hợp lệ giữa tất cả các luồng.
Joelmob

Chuỗi không thay đổi, nhưng nó được đọc từ bộ nhớ (việc ghi "!"không bao giờ xảy ra) trong phương thức đầu tiên - phương thức đầu tiên tương đương với phân lớp con Threadvà cung cấp cho nó một trường tùy chỉnh. Điểm chuẩn đo lường trường hợp cạnh cực hạn trong đó toàn bộ tính toán bao gồm việc đọc cục bộ biến / luồng - các ứng dụng thực có thể không bị ảnh hưởng tùy thuộc vào mẫu truy cập của chúng, nhưng trong trường hợp xấu nhất, chúng sẽ hoạt động như trên.
axel22

4

Đây là một bài kiểm tra khác. Kết quả cho thấy ThreadLocal chậm hơn một chút so với trường thông thường, nhưng theo cùng một thứ tự. Chậm hơn khoảng 12%

public class Test {
private static final int N = 100000000;
private static int fieldExecTime = 0;
private static int threadLocalExecTime = 0;

public static void main(String[] args) throws InterruptedException {
    int execs = 10;
    for (int i = 0; i < execs; i++) {
        new FieldExample().run(i);
        new ThreadLocaldExample().run(i);
    }
    System.out.println("Field avg:"+(fieldExecTime / execs));
    System.out.println("ThreadLocal avg:"+(threadLocalExecTime / execs));
}

private static class FieldExample {
    private Map<String,String> map = new HashMap<String, String>();

    public void run(int z) {
        System.out.println(z+"-Running  field sample");
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++){
            String s = Integer.toString(i);
            map.put(s,"a");
            map.remove(s);
        }
        long end = System.currentTimeMillis();
        long t = (end - start);
        fieldExecTime += t;
        System.out.println(z+"-End field sample:"+t);
    }
}

private static class ThreadLocaldExample{
    private ThreadLocal<Map<String,String>> myThreadLocal = new ThreadLocal<Map<String,String>>() {
        @Override protected Map<String, String> initialValue() {
            return new HashMap<String, String>();
        }
    };

    public void run(int z) {
        System.out.println(z+"-Running thread local sample");
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++){
            String s = Integer.toString(i);
            myThreadLocal.get().put(s, "a");
            myThreadLocal.get().remove(s);
        }
        long end = System.currentTimeMillis();
        long t = (end - start);
        threadLocalExecTime += t;
        System.out.println(z+"-End thread local sample:"+t);
    }
}
}'

Đầu ra:

0-Mẫu trường chạy

Mẫu trường 0-End: 6044

0-Chạy mẫu cục bộ chuỗi

Mẫu cục bộ của chủ đề 0-End: 6015

1-Mẫu trường chạy

Mẫu trường 1 đầu: 5095

1-Chạy mẫu cục bộ

Mẫu cục bộ chủ đề 1 đầu: 5720

2-Mẫu trường chạy

Mẫu trường 2 đầu: 4842

2-Chạy mẫu cục bộ

Mẫu cục bộ ren 2 đầu: 5835

3-Mẫu trường chạy

Mẫu trường 3 đầu: 4674

3-Chạy mẫu cục bộ

Mẫu cục bộ 3 đầu sợi: 5287

4-Mẫu trường chạy

Mẫu trường 4 cuối: 4849

4-Chạy mẫu cục bộ

Mẫu cục bộ chủ đề 4 đầu: 5309

5-Mẫu trường chạy

Mẫu trường 5 cuối: 4781

5-Chạy mẫu cục bộ

Mẫu cục bộ 5 đầu sợi: 5330

6-Mẫu trường chạy

6-Mẫu trường cuối: 5294

6-Chạy mẫu cục bộ luồng

Mẫu cục bộ 6 đầu sợi: 5511

7-Mẫu trường chạy

7-Mẫu trường cuối: 5119

7-Chạy mẫu cục bộ luồng

Mẫu cục bộ của chuỗi 7 đầu: 5793

8-Mẫu trường chạy

8-Mẫu trường cuối: 4977

8-Chạy mẫu cục bộ luồng

8-End thread local sample: 6374

9-Mẫu trường chạy

9-Mẫu trường cuối: 4841

9-Chạy mẫu cục bộ

Mẫu cục bộ của chuỗi 9 đầu: 5471

Trường trung bình: 5051

ThreadLocal trung bình: 5664

Env:

phiên bản openjdk "1.8.0_131"

CPU Intel® Core ™ i7-7500U @ 2,70GHz × 4

Ubuntu 16.04 LTS


Xin lỗi, đây thậm chí không phải là một bài kiểm tra hợp lệ. A) Vấn đề lớn nhất: bạn đang phân bổ Chuỗi với mỗi lần lặp ( Int.toString), điều này cực kỳ tốn kém so với những gì bạn đang thử nghiệm. B) bạn đang thực hiện hai hoạt động bản đồ mỗi lần lặp, cũng hoàn toàn không liên quan và tốn kém. Thay vào đó, hãy thử tăng một int nguyên thủy từ ThreadLocal. C) Sử dụng System.nanoTimethay vì System.currentTimeMillis, cái trước là để lập hồ sơ, cái sau dành cho mục đích ngày giờ của người dùng và có thể thay đổi dưới chân bạn. D) Bạn nên tránh phân bổ hoàn toàn, bao gồm cả những phân bổ cấp cao nhất cho các lớp "ví dụ" của bạn
Philip Guin

3

@Pete là thử nghiệm chính xác trước khi bạn tối ưu hóa.

Tôi sẽ rất ngạc nhiên nếu việc xây dựng MessageDigest có bất kỳ chi phí nghiêm trọng nào khi so sánh với việc sử dụng nó thực sự.

Việc không sử dụng ThreadLocal có thể là một nguồn rò rỉ và các tham chiếu lủng lẳng, không có vòng đời rõ ràng, nói chung là tôi không bao giờ sử dụng ThreadLocal mà không có kế hoạch rõ ràng về thời điểm một tài nguyên cụ thể sẽ bị xóa.


0

Xây dựng nó và đo lường nó.

Ngoài ra, bạn chỉ cần một threadlocal nếu bạn đóng gói hành vi tiêu hóa thông điệp của mình vào một đối tượng. Nếu bạn cần một MessageDigest cục bộ và một byte cục bộ [1000] cho một số mục đích, hãy tạo một đối tượng với trường messageDigest và một byte [] và đặt đối tượng đó vào ThreadLocal thay vì cả hai riêng lẻ.


Cảm ơn, MessageDigest và byte [] là cách sử dụng khác nhau, vì vậy không cần một đối tượng.
Sarmun
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.