ประสิทธิภาพของตัวแปร ThreadLocal


87

อ่านจากThreadLocalตัวแปรช้ากว่าจากฟิลด์ปกติมากแค่ไหน?

การสร้างวัตถุอย่างง่ายเร็วกว่าหรือช้ากว่าการเข้าถึงThreadLocalตัวแปรคืออะไร?

ฉันคิดว่ามันเร็วพอที่จะมีThreadLocal<MessageDigest>อินสแตนซ์ได้เร็วขึ้นมากจากนั้นสร้างอินสแตนซ์MessageDigestทุกครั้ง แต่นั่นยังใช้กับไบต์ [10] หรือไบต์ [1000] ด้วยเช่นกัน

แก้ไข: คำถามคือเกิดอะไรขึ้นเมื่อโทรไปThreadLocal? ถ้านั่นเป็นเพียงสนามเช่นอื่น ๆ คำตอบก็จะเป็น "มันเร็วที่สุดเสมอ" ใช่ไหม?


2
เธรดโลคัลเป็นฟิลด์พื้นฐานที่มีแฮชแมปและการค้นหาโดยที่คีย์คืออ็อบเจ็กต์เธรดปัจจุบัน จึงช้ากว่ามาก แต่ก็ยังเร็ว :)
eckes

1
@eckes: มันมีพฤติกรรมเช่นนั้นอย่างแน่นอน แต่โดยปกติแล้วจะไม่ใช้วิธีนี้ แทนที่จะThreadมีแฮชแมป (ไม่ซิงโครไนซ์) โดยที่คีย์คือThreadLocalวัตถุปัจจุบัน
sbk

คำตอบ:


40

การรันมาตรฐานที่ไม่ได้เผยแพร่ThreadLocal.getใช้เวลาประมาณ 35 รอบต่อการทำซ้ำบนเครื่องของฉัน ไม่ใช่เรื่องใหญ่ ในการนำซันไปใช้งานแผนที่แฮชการตรวจสอบเชิงเส้นแบบกำหนดเองในThreadแมปThreadLocals กับค่า เนื่องจากสามารถเข้าถึงได้ด้วยเธรดเดียวเท่านั้นจึงสามารถเข้าถึงได้เร็วมาก

การจัดสรรวัตถุขนาดเล็กจะใช้จำนวนรอบที่ใกล้เคียงกันแม้ว่าเนื่องจากความเหนื่อยล้าของแคชคุณอาจได้ตัวเลขที่ลดลงบ้างในวงที่แน่น

การก่อสร้างMessageDigestมีแนวโน้มที่จะมีราคาค่อนข้างแพง มีรัฐจำนวนพอสมควรและการก่อสร้างต้องผ่านProviderกลไก SPI คุณอาจสามารถเพิ่มประสิทธิภาพโดยตัวอย่างเช่นการโคลนหรือการให้ไฟล์Provider.

เพียงเพราะแคชในการThreadLocalสร้างมากกว่าสร้างอาจเร็วกว่าไม่ได้หมายความว่าประสิทธิภาพของระบบจะเพิ่มขึ้น คุณจะมีค่าโสหุ้ยเพิ่มเติมที่เกี่ยวข้องกับ GC ซึ่งทำให้ทุกอย่างช้าลง

เว้นแต่ว่าแอปพลิเคชันของคุณจะใช้งานหนักมากMessageDigestคุณอาจต้องการพิจารณาใช้แคชเธรดแบบเดิม


5
IMHO วิธีที่เร็วที่สุดเป็นเพียงที่จะไม่สนใจ SPI new org.bouncycastle.crypto.digests.SHA1Digest()และการใช้งานบางอย่างเช่น ฉันค่อนข้างแน่ใจว่าไม่มีแคชเอาชนะมันได้
maaartinus

57

ในปี 2009 JVM บางตัวนำมาThreadLocalใช้โดยไม่ได้ซิงโครไนซ์HashMapในThread.currentThread()ออบเจ็กต์ สิ่งนี้ทำให้มันเร็วมาก (แม้ว่าจะไม่เร็วเท่าการใช้การเข้าถึงฟิลด์ปกติก็ตาม) รวมถึงการตรวจสอบให้แน่ใจว่าThreadLocalวัตถุได้รับการจัดระเบียบเมื่อThreadเสียชีวิต การอัปเดตคำตอบนี้ในปี 2559 ดูเหมือนว่า JVM รุ่นใหม่ส่วนใหญ่ (ทั้งหมด?) จะใช้ a ThreadLocalMapกับการตรวจสอบเชิงเส้น ฉันไม่แน่ใจเกี่ยวกับประสิทธิภาพของสิ่งเหล่านี้ - แต่ฉันนึกไม่ออกว่ามันแย่กว่าการใช้งานก่อนหน้านี้อย่างมีนัยสำคัญ

แน่นอนว่าnew Object()ทุกวันนี้เร็วมากเช่นกันและคนเก็บขยะก็สามารถเรียกคืนวัตถุอายุสั้นได้ดีมาก

เว้นแต่คุณจะแน่ใจว่าการสร้างอ็อบเจ็กต์นั้นจะมีราคาแพงหรือคุณจำเป็นต้องคงสถานะบางอย่างบนเธรดตามเธรดคุณจะดีกว่าที่จะจัดสรรโซลูชันที่ง่ายกว่าเมื่อจำเป็นและจะเปลี่ยนไปThreadLocalใช้การใช้งานเมื่อโพรไฟล์ บอกคุณว่าคุณต้องทำ


4
+1 เพื่อเป็นคำตอบเดียวที่ตอบคำถามได้จริง
cletus

คุณช่วยยกตัวอย่าง JVM สมัยใหม่ที่ไม่ใช้ linear probing สำหรับ ThreadLocalMap ได้ไหม Java 8 OpenJDK ยังคงใช้ ThreadLocalMap กับการตรวจสอบเชิงเส้น grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick

1
@Karthick ขอโทษที่ฉันทำไม่ได้ ฉันเขียนสิ่งนี้ในปี 2009 ฉันจะอัปเดต
Bill Michell

34

เป็นคำถามที่ดีฉันถามตัวเองเมื่อเร็ว ๆ นี้ เพื่อให้คุณได้ตัวเลขที่ชัดเจนเกณฑ์มาตรฐานด้านล่าง (ใน Scala รวบรวมเป็นไบต์โค้ดเดียวกันกับโค้ด Java ที่เทียบเท่า):

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

มีให้ที่นี่ดำเนินการบน AMD 4x 2.8 GHz dual-cores และ quad-core i7 พร้อมไฮเปอร์เธรด (2.67 GHz)

นี่คือตัวเลข:

i7

ข้อมูลจำเพาะ: Intel i7 2x quad-core @ 2.67 GHz Test: scala.threads.ParallelTests

ชื่อการทดสอบ: loop_heap_read

หมายเลขเธรด: 1 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 9.0069 9.0036 9.0017 9.0084 9.0074 (เฉลี่ย = 9.1034 นาที = 8.9986 สูงสุด = 21.0306)

หมายเลขเธรด: 2 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 4.5563 4.7128 4.5663 4.5617 4.5724 (เฉลี่ย = 4.6337 นาที = 4.5509 สูงสุด = 13.9476)

หมายเลขเธรด: 4 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 2.3946 2.3979 2.3934 2.3937 2.3964 (เฉลี่ย = 2.5113 นาที = 2.3884 สูงสุด = 13.5496)

หมายเลขเธรด: 8 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 2.4479 2.4362 2.4323 2.4472 2.4383 (เฉลี่ย = 2.5562 นาที = 2.4166 สูงสุด = 10.3726)

ชื่อการทดสอบ: threadlocal

หมายเลขเธรด: 1 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 91.1741 90.8978 90.6181 90.6200 90.6113 (เฉลี่ย = 91.0291 นาที = 90.6000 สูงสุด = 129.7501)

หมายเลขเธรด: 2 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 45.3838 45.3858 45.6676 45.3772 45.3839 (เฉลี่ย = 46.0555 นาที = 45.3726 สูงสุด = 90.7108)

หมายเลขเธรด: 4 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 22.8118 22.8135 59.1753 22.8229 22.8172 (เฉลี่ย = 23.9752 นาที = 22.7951 สูงสุด = 59.1753)

หมายเลขเธรด: 8 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 22.2965 22.2415 22.3438 22.3109 22.4460 (เฉลี่ย = 23.2676 นาที = 22.2346 สูงสุด = 50.3583)

AMD

ข้อมูลจำเพาะ: AMD 8220 4x dual-core @ 2.8 GHz Test: scala.threads.ParallelTests

ชื่อการทดสอบ: loop_heap_read

งานทั้งหมด: 20000000 เธรดจำนวน: 1 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 12.625 12.631 12.634 12.632 12.628 (เฉลี่ย = 12.7333 นาที = 12.619 สูงสุด = 26.698)

ชื่อการทดสอบ: loop_heap_read งานทั้งหมด: 20000000

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 6.412 6.424 6.408 6.397 6.43 (เฉลี่ย = 6.5367 นาที = 6.393 สูงสุด = 19.716)

หมายเลขเธรด: 4 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 3.385 4.298 9.7 6.535 3.385 (เฉลี่ย = 5.6079 นาที = 3.354 สูงสุด = 21.603)

หมายเลขเธรด: 8 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 5.389 5.795 10.818 3.823 3.824 (เฉลี่ย = 5.5810 นาที = 2.405 สูงสุด = 19.755)

ชื่อการทดสอบ: threadlocal

หมายเลขเธรด: 1 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 200.217 207.335 200.241 207.342 200.23 (เฉลี่ย = 202.2424 นาที = 200.184 สูงสุด = 245.369)

หมายเลขเธรด: 2 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 100.208 100.199 100.211 103.781 100.215 (เฉลี่ย = 102.2238 นาที = 100.192 สูงสุด = 129.505)

หมายเลขเธรด: 4 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 62.101 67.629 62.087 52.021 55.766 (เฉลี่ย = 65.6361 นาที = 50.282 สูงสุด = 167.433)

หมายเลขเธรด: 8 การทดสอบทั้งหมด: 200

เวลาทำงาน: (แสดง 5 ครั้งสุดท้าย) 40.672 74.301 34.434 41.549 28.119 (เฉลี่ย = 54.7701 นาที = 28.119 สูงสุด = 94.424)

สรุป

เธรดโลคัลอยู่ที่ประมาณ 10-20x ของฮีปที่อ่าน ดูเหมือนว่าจะปรับขนาดได้ดีในการนำ JVM นี้ไปใช้และสถาปัตยกรรมเหล่านี้ด้วยจำนวนโปรเซสเซอร์


5
+1 ความชื่นชอบในการเป็นหนึ่งเดียวที่ให้ผลลัพธ์เชิงปริมาณ ฉันค่อนข้างสงสัยเพราะการทดสอบเหล่านี้อยู่ใน Scala แต่อย่างที่คุณบอกว่า bytecodes ของ Java ควรจะคล้ายกัน ...
Gravity

ขอบคุณ! สิ่งนี้ในขณะที่ลูปส่งผลให้เกิด bytecode เกือบเดียวกันกับที่โค้ด Java ที่เกี่ยวข้องจะสร้างขึ้น เวลาที่แตกต่างกันสามารถสังเกตได้ใน VM ที่แตกต่างกัน - สิ่งนี้ได้รับการทดสอบใน Sun JVM1.6
axel22

รหัสมาตรฐานนี้ไม่ได้จำลองกรณีการใช้งานที่ดีสำหรับ ThreadLocal ในวิธีแรก: ทุกเธรดจะมีการแสดงร่วมกันในหน่วยความจำสตริงจะไม่เปลี่ยนแปลง ในวิธีที่สองคุณเปรียบเทียบค่าใช้จ่ายของการค้นหาแฮชแท็กโดยที่สตริงไม่เชื่อมต่อระหว่างเธรดทั้งหมด
Joelmob

สตริงไม่เปลี่ยนแปลง แต่อ่านจากหน่วยความจำ (การเขียน"!"ไม่เคยเกิดขึ้น) ในวิธีแรก - วิธีแรกมีประสิทธิภาพเทียบเท่ากับคลาสย่อยThreadและให้ฟิลด์ที่กำหนดเอง เกณฑ์มาตรฐานจะวัดกรณีขอบที่รุนแรงซึ่งการคำนวณทั้งหมดประกอบด้วยการอ่านตัวแปร / เธรดโลคัล - แอปพลิเคชันจริงอาจไม่ได้รับผลกระทบขึ้นอยู่กับรูปแบบการเข้าถึงของพวกเขา แต่ในกรณีที่เลวร้ายที่สุดพวกเขาจะทำงานตามที่กล่าว
axel22

4

นี่เป็นการทดสอบอีกครั้ง ผลลัพธ์แสดงให้เห็นว่า ThreadLocal ช้ากว่าฟิลด์ปกติเล็กน้อย แต่อยู่ในลำดับเดียวกัน Aprox ช้าลง 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);
    }
}
}'

เอาท์พุต:

0- รันฟิลด์ตัวอย่าง

ตัวอย่างฟิลด์ 0-End: 6044

0- รันเธรดท้องถิ่นตัวอย่าง

ตัวอย่างโลคัลเธรด 0-End: 6015

ตัวอย่างฟิลด์ 1 รัน

ตัวอย่างฟิลด์ 1-End: 5095

ตัวอย่างโลคัลเธรดที่รันอยู่ 1 รายการ

ตัวอย่างโลคัลเธรดแบบ 1-End: 5720

ตัวอย่างฟิลด์ 2 รัน

ตัวอย่างฟิลด์ 2-End: 4842

ตัวอย่างโลคัลเธรดที่รันอยู่ 2 รายการ

ตัวอย่างโลคัลเธรด 2-End: 5835

ตัวอย่างฟิลด์ 3 รัน

ตัวอย่างฟิลด์ 3-End: 4674

ตัวอย่างโลคัลเธรด 3 รัน

ตัวอย่างโลคัลเธรด 3-End: 5287

ตัวอย่างสนาม 4 รัน

ตัวอย่างฟิลด์ 4-End: 4849

ตัวอย่างโลคัลเธรด 4 รัน

ตัวอย่างโลคัลเธรด 4-End: 5309

ตัวอย่างสนาม 5 รัน

ตัวอย่างฟิลด์ 5-End: 4781

ตัวอย่างโลคัลเธรด 5 รัน

ตัวอย่างโลคัลเธรด 5-End: 5330

ตัวอย่างสนาม 6 รัน

ตัวอย่างฟิลด์ 6-End: 5294

ตัวอย่างโลคัลเธรด 6 รัน

ตัวอย่างโลคัลเธรด 6-End: 5511

ตัวอย่างสนาม 7 วิ่ง

ตัวอย่างฟิลด์ 7-End: 5119

ตัวอย่างโลคัลเธรด 7 รัน

ตัวอย่างโลคัลเธรด 7-End: 5793

ตัวอย่างสนาม 8 รัน

ตัวอย่างฟิลด์ 8-End: 4977

ตัวอย่างโลคัลเธรด 8 รัน

ตัวอย่างโลคัลเธรด 8-End: 6374

ตัวอย่างสนาม 9 ช่อง

ตัวอย่างฟิลด์ 9-End: 4841

ตัวอย่างโลคัลเธรดที่รัน 9 รายการ

ตัวอย่างโลคัลเธรด 9-End: 5471

ค่าเฉลี่ยฟิลด์: 5051

ThreadLocal เฉลี่ย: 5664

Env:

openjdk เวอร์ชัน "1.8.0_131"

Intel® Core ™ i7-7500U CPU ที่ 2.70GHz × 4

Ubuntu 16.04 LTS


1
ขออภัยนี่ยังไม่ใกล้เคียงกับการทดสอบที่ใช้ได้ A) ปัญหาที่ใหญ่ที่สุด: คุณกำลังจัดสรร Strings ด้วยการทำซ้ำทุกครั้ง ( Int.toString)ซึ่งมีราคาแพงมากเมื่อเทียบกับสิ่งที่คุณกำลังทดสอบ B) คุณกำลังทำแผนที่สองครั้งในการทำซ้ำทุกครั้งซึ่งไม่เกี่ยวข้องกันเลยและมีราคาแพง ลองเพิ่ม int ดั้งเดิมจาก ThreadLocal แทน C) ใช้System.nanoTimeแทนSystem.currentTimeMillisอดีตสำหรับโปรไฟล์หลังสำหรับผู้ใช้เพื่อวัตถุประสงค์ในวันเวลาและสามารถเปลี่ยนแปลงภายใต้เท้าของคุณ D) คุณควรหลีกเลี่ยงการจัดสรรทั้งหมดรวมถึงคลาสระดับบนสุดสำหรับคลาส "ตัวอย่าง" ของคุณ
Philip Guin

3

@Pete คือการทดสอบที่ถูกต้องก่อนที่คุณจะปรับให้เหมาะสม

ฉันจะแปลกใจมากถ้าการสร้าง MessageDigest มีค่าใช้จ่ายที่ร้ายแรงเมื่อเทียบกับการใช้งานจริง

การพลาดการใช้ ThreadLocal อาจเป็นแหล่งที่มาของการรั่วไหลและการอ้างอิงที่ห้อยลงซึ่งไม่มีวงจรชีวิตที่ชัดเจนโดยทั่วไปฉันไม่เคยใช้ ThreadLocal โดยไม่มีแผนชัดเจนว่าจะนำทรัพยากรใดออกเมื่อใด


0

สร้างและวัดผล

นอกจากนี้คุณต้องมี threadlocal เพียงรายการเดียวหากคุณห่อหุ้มพฤติกรรมการย่อยข้อความลงในวัตถุ หากคุณต้องการ MessageDigest แบบโลคัลและ local byte [1000] เพื่อจุดประสงค์บางอย่างให้สร้างอ็อบเจ็กต์ด้วยฟิลด์ messageDigest และไบต์ [] และใส่อ็อบเจ็กต์นั้นลงใน ThreadLocal แทนที่จะเป็นทั้งสองแบบทีละรายการ


ขอบคุณ MessageDigest และ byte [] เป็นการใช้งานที่แตกต่างกันดังนั้นจึงไม่จำเป็นต้องใช้วัตถุเดียว
Sarmun
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.