เหตุใด Java จึงเปิดสวิตช์ int ที่ต่อเนื่องกันจึงปรากฏว่าทำงานเร็วขึ้นพร้อมกับเคสเพิ่มเติม


276

ฉันกำลังทำงานกับโค้ด Java บางตัวซึ่งจำเป็นต้องปรับให้เหมาะสมอย่างมากเนื่องจากมันจะทำงานในฟังก์ชั่นฮ็อตที่มีการเรียกใช้หลาย ๆ จุดในลอจิกโปรแกรมหลักของฉัน เป็นส่วนหนึ่งของรหัสนี้เกี่ยวข้องกับการคูณdoubleตัวแปรโดย10ยกพลไม่ใช่เชิงลบint exponents วิธีหนึ่งที่รวดเร็ว (แก้ไข: แต่ไม่ใช่วิธีที่เร็วที่สุดโปรดดูอัปเดตที่ 2 ด้านล่าง) เพื่อรับค่าที่คูณได้switchในexponent:

double multiplyByPowerOfTen(final double d, final int exponent) {
   switch (exponent) {
      case 0:
         return d;
      case 1:
         return d*10;
      case 2:
         return d*100;
      // ... same pattern
      case 9:
         return d*1000000000;
      case 10:
         return d*10000000000L;
      // ... same pattern with long literals
      case 18:
         return d*1000000000000000000L;
      default:
         throw new ParseException("Unhandled power of ten " + power, 0);
   }
}

จุดไข่ปลาที่แสดงความคิดเห็นข้างต้นบ่งชี้ว่าcase intค่าคงที่ยังคงเพิ่มขึ้นอย่างต่อเนื่องโดย 1 ดังนั้นจึงมี 19 caseวินาทีในตัวอย่างโค้ดข้างต้น ตั้งแต่ผมไม่แน่ใจว่าที่จริงผมจะต้องอำนาจทั้งหมด 10 ในcaseงบ10ผ่าน18ฉันวิ่ง microbenchmarks บางเปรียบเทียบเวลาที่จะเสร็จสมบูรณ์ 10 ล้านการดำเนินงานที่มีนี้switchคำสั่งเมื่อเทียบกับswitchมีเพียงcases 0ผ่าน9(ที่มีexponentจำกัด ถึง 9 หรือน้อยไป หลีกเลี่ยงการทำลาย pared-down switch) ฉันได้ผลลัพธ์ที่ค่อนข้างน่าประหลาดใจ (สำหรับฉันอย่างน้อย!) ว่าการที่ข้อความยาวswitchcaseวิ่งเร็วกว่าจริง ๆ

บน lark ฉันพยายามเพิ่มcases มากขึ้นซึ่งเพิ่งส่งคืนค่า dummy และพบว่าฉันสามารถสลับให้ทำงานได้เร็วขึ้นด้วยการประกาศประมาณ 22-27 cases (แม้ว่ากรณี dummy เหล่านั้นจะไม่ตีจริงในขณะที่รหัสกำลังทำงานอยู่ ) (อีกครั้งcases ถูกเพิ่มเข้ามาในรูปแบบที่ต่อเนื่องกันโดยการเพิ่มcaseค่าคงที่ก่อนหน้า1นี้) ความแตกต่างของเวลาดำเนินการเหล่านี้ไม่มีความสำคัญมาก: สำหรับการสุ่มexponentระหว่าง0และคำสั่ง10dummy padded switchเวอร์ชั่นสำหรับการออมทั้งหมด 5ns ต่อการดำเนินการ ดังนั้นไม่ใช่สิ่งที่ทำให้หมกมุ่นมากกว่าการขยายกswitchคำสั่งที่คุ้มค่าความพยายามจากมุมมองการเพิ่มประสิทธิภาพ แต่ฉันก็ยังเพิ่งพบว่ามันอยากรู้อยากเห็นและตอบโต้ง่าย ๆ ที่ a switchไม่ช้าลง (หรือบางทีอาจจะดีที่สุดในการรักษาO (1)เวลา) ให้ทำงานเมื่อcaseมีการเพิ่มจำนวนมากขึ้น

สลับผลการเปรียบเทียบ

เหล่านี้คือผลลัพธ์ที่ฉันได้รับจากการรันพร้อมข้อ จำกัด ต่างๆเกี่ยวกับexponentค่าที่สร้างแบบสุ่ม ฉันไม่ได้รวมผลการค้นหาในทุกทางลงไป1สำหรับexponentขีด จำกัด แต่รูปร่างทั่วไปของเส้นโค้งยังคงเหมือนเดิมกับสันเขารอบเครื่องหมาย 12-17 กรณีและหุบเขาระหว่าง 18-28 การทดสอบทั้งหมดถูกเรียกใช้ใน JUnitBenchmarks โดยใช้คอนเทนเนอร์ที่ใช้ร่วมกันสำหรับค่าสุ่มเพื่อให้แน่ใจว่าอินพุตการทดสอบที่เหมือนกัน ฉันยังรันการทดสอบทั้งตามลำดับจากคำสั่งที่ยาวที่สุดswitchถึงสั้นที่สุดและในทางกลับกันเพื่อลองและกำจัดความเป็นไปได้ของปัญหาการทดสอบที่เกี่ยวข้องกับการสั่งซื้อ ฉันได้วางโค้ดการทดสอบของฉันลงบน repo GitHub หากใครต้องการลองผลลัพธ์เหล่านี้ซ้ำ

แล้วเกิดอะไรขึ้นที่นี่? ความหลากหลายของสถาปัตยกรรมหรือการสร้างเกณฑ์มาตรฐานขนาดเล็กของฉัน? หรือเป็น Java switchจริงๆเล็ก ๆ น้อย ๆ ได้เร็วขึ้นในการดำเนินการใน18การ28 caseช่วงกว่านั้นก็คือจากการ11ขึ้นไป17?

gitub ทดสอบ repo "สวิตช์ทดลอง"

UPDATE:ฉันทำความสะอาดไลบรารีการเปรียบเทียบค่อนข้างน้อยและเพิ่มไฟล์ข้อความใน / ผลลัพธ์ด้วยผลลัพธ์บางอย่างในช่วงกว้างของexponentค่าที่เป็นไปได้ ฉันยังเพิ่มตัวเลือกในรหัสการทดสอบไม่ให้โยนเป็นนักการExceptionจากdefaultแต่นี้ไม่ปรากฏขึ้นส่งผลกระทบต่อผลการค้นหา

UPDATE 2:พบบางสนทนาที่ดีงามของปัญหานี้มาจากด้านหลังในปี 2009 ในฟอรั่ xkcd ที่นี่: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 การสนทนาของ OP เกี่ยวกับการใช้Array.binarySearch()ทำให้ฉันมีความคิดสำหรับการใช้งานแบบอาเรย์อย่างง่ายของรูปแบบการยกกำลังด้านบน ไม่จำเป็นต้องค้นหาแบบไบนารี่เพราะฉันรู้ว่าสิ่งที่อยู่ในarrayนั้น ดูเหมือนว่าจะทำงานได้เร็วกว่าการใช้งานประมาณ 3 เท่าโดยswitchเห็นได้ชัดว่ามีค่าใช้จ่ายของโฟลว์การควบคุมบางอย่างที่switchกำบัง รหัสนั้นถูกเพิ่มไปยัง repo Github ด้วย


64
ตอนนี้ชาว Google ทุกคนทุกที่จะมี 22 กรณีอย่างแม่นยำในทุกswitchข้อความเพราะเป็นทางออกที่ดีที่สุดอย่างชัดเจน : D (อย่าแสดงสิ่งนี้
ต่อหน้า

2
คุณมี SSCCE ที่ง่ายกว่านี้ไหม อันนี้ไม่ได้รวบรวมสำหรับฉัน อ่อนแออย่างที่ฉันเป็นด้วยประสิทธิภาพของ Java ฉันต้องการถ่ายภาพนี้
Mysticial

5
คุณอาจพบว่าส่วน"สวิตช์ใน JVM" ในคำตอบของฉันเกี่ยวกับกรณีที่ใช้สตริงเป็นประโยชน์ ผมคิดว่าสิ่งที่เกิดขึ้นที่นี่เป็นที่ที่คุณจะเปลี่ยนจากไปlookupswitch tableswitchการแยกส่วนโค้ดของคุณด้วยjavapจะแสดงให้คุณเห็นอย่างแน่นอน
erickson

2
ฉันเพิ่มไหการพึ่งพาลงในโฟลเดอร์ / lib ใน repo @Master อย่างเป็นทางการขออภัยฉันใช้เวลาไปกับการลงหลุมกระต่ายมากเกินไปแล้ว! หากคุณใช้ "ขยาย AbstractBenchmark" ออกจากคลาสทดสอบและกำจัดการนำเข้า "com.carrotsearch" คุณสามารถเรียกใช้เพียงการพึ่งพา JUnit แต่เนื้อหา carrotsearch ค่อนข้างดีสำหรับการกรองสัญญาณรบกวนบางส่วนจาก JIT และช่วงเวลาอุ่นเครื่อง น่าเสียดายที่ฉันไม่รู้วิธีเรียกใช้การทดสอบ JUnit เหล่านี้นอก IntelliJ
Andrew Bissell

2
@AndrewBissell ฉันจัดการเพื่อทำซ้ำผลลัพธ์ของคุณด้วยมาตรฐานที่ง่ายกว่ามาก สาขาเทียบกับตารางสำหรับประสิทธิภาพขนาดเล็กกับขนาดกลางค่อนข้างคาดเดาได้ชัดเจน แต่ฉันไม่มีความเข้าใจที่ดีไปกว่าคนอื่น ๆ เกี่ยวกับการแช่ตัวใน 30 กรณี ...
Mysticial

คำตอบ:


228

ตามที่อธิบายโดยคำตอบอื่น ๆเนื่องจากค่าของเคสมีความต่อเนื่องกัน (ต่างจากกระจัดกระจาย) bytecode ที่สร้างขึ้นสำหรับการทดสอบต่างๆของคุณจะใช้ตารางสวิตช์ (คำสั่ง bytecode tableswitch)

อย่างไรก็ตามเมื่อ JIT เริ่มงานและรวบรวม bytecode เป็นชุดประกอบtableswitchคำสั่งจะไม่ส่งผลให้เกิดพอยน์เตอร์พอยน์เตอร์เสมอไปบางครั้งตารางสวิตช์จะถูกเปลี่ยนเป็นสิ่งที่ดูเหมือนlookupswitch(คล้ายกับif/ else ifโครงสร้าง)

การแยกแอสเซมบลีที่สร้างโดย JIT (ฮอตสปอต JDK 1.7) แสดงให้เห็นว่าจะใช้การทดแทน if / อื่นถ้าเมื่อมี 17 กรณีหรือน้อยกว่าอาร์เรย์ของพอยน์เตอร์เมื่อมีมากกว่า 18 (มีประสิทธิภาพมากขึ้น)

สาเหตุที่ใช้หมายเลขวิเศษ 18 นี้ดูเหมือนจะลดลงไปเป็นค่าเริ่มต้นของการMinJumpTableSizeตั้งค่าสถานะ JVM (ประมาณ 352 บรรทัดในรหัส)

ฉันได้หยิบยกปัญหาขึ้นมาในรายการคอมไพเลอร์ฮอตสปอตแล้ว มันน่าจะเป็นมรดกของการทดสอบที่ผ่านมา โปรดทราบว่าค่าเริ่มต้นนี้ถูกลบใน JDK 8หลังจากทำการเปรียบเทียบเพิ่มเติมแล้ว

ในที่สุดเมื่อวิธีการยาวเกินไป (> 25 รายในการทดสอบของฉัน) มันไม่ได้ถูกแทรกเข้าไปด้วยการตั้งค่า JVM เริ่มต้นอีกต่อไปซึ่งเป็นสาเหตุที่เป็นไปได้มากที่สุดสำหรับการลดลงของประสิทธิภาพ ณ จุดนั้น


ด้วย 5 กรณีรหัสที่ถอดรหัสแล้วจะมีลักษณะดังนี้ (สังเกตคำแนะนำของ cmp / je / jg / jmp ซึ่งเป็นชุดประกอบสำหรับ if / goto):

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x00000000024f0160: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x00000000024f0167: push   rbp
  0x00000000024f0168: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x00000000024f016c: cmp    edx,0x3
  0x00000000024f016f: je     0x00000000024f01c3
  0x00000000024f0171: cmp    edx,0x3
  0x00000000024f0174: jg     0x00000000024f01a5
  0x00000000024f0176: cmp    edx,0x1
  0x00000000024f0179: je     0x00000000024f019b
  0x00000000024f017b: cmp    edx,0x1
  0x00000000024f017e: jg     0x00000000024f0191
  0x00000000024f0180: test   edx,edx
  0x00000000024f0182: je     0x00000000024f01cb
  0x00000000024f0184: mov    ebp,edx
  0x00000000024f0186: mov    edx,0x17
  0x00000000024f018b: call   0x00000000024c90a0  ; OopMap{off=48}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
                                                ;   {runtime_call}
  0x00000000024f0190: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
  0x00000000024f0191: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffffa7]        # 0x00000000024f0140
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@52 (line 62)
                                                ;   {section_word}
  0x00000000024f0199: jmp    0x00000000024f01cb
  0x00000000024f019b: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff8d]        # 0x00000000024f0130
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@46 (line 60)
                                                ;   {section_word}
  0x00000000024f01a3: jmp    0x00000000024f01cb
  0x00000000024f01a5: cmp    edx,0x5
  0x00000000024f01a8: je     0x00000000024f01b9
  0x00000000024f01aa: cmp    edx,0x5
  0x00000000024f01ad: jg     0x00000000024f0184  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x00000000024f01af: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff81]        # 0x00000000024f0138
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@64 (line 66)
                                                ;   {section_word}
  0x00000000024f01b7: jmp    0x00000000024f01cb
  0x00000000024f01b9: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff67]        # 0x00000000024f0128
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@70 (line 68)
                                                ;   {section_word}
  0x00000000024f01c1: jmp    0x00000000024f01cb
  0x00000000024f01c3: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff55]        # 0x00000000024f0120
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x00000000024f01cb: add    rsp,0x10
  0x00000000024f01cf: pop    rbp
  0x00000000024f01d0: test   DWORD PTR [rip+0xfffffffffdf3fe2a],eax        # 0x0000000000430000
                                                ;   {poll_return}
  0x00000000024f01d6: ret    

ด้วย 18 กรณีแอสเซมบลีที่มีลักษณะเช่นนี้ (สังเกตเห็นอาร์เรย์ของพอยน์เตอร์ที่ใช้และระงับความต้องการเปรียบเทียบทั้งหมด: jmp QWORD PTR [r8+r10*1]ข้ามไปยังการคูณที่ถูกต้องโดยตรง) - นั่นคือเหตุผลที่เป็นไปได้สำหรับการปรับปรุงประสิทธิภาพ:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000287fe20: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x000000000287fe27: push   rbp
  0x000000000287fe28: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000287fe2c: cmp    edx,0x13
  0x000000000287fe2f: jae    0x000000000287fe46
  0x000000000287fe31: movsxd r10,edx
  0x000000000287fe34: shl    r10,0x3
  0x000000000287fe38: movabs r8,0x287fd70       ;   {section_word}
  0x000000000287fe42: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x000000000287fe46: mov    ebp,edx
  0x000000000287fe48: mov    edx,0x31
  0x000000000287fe4d: xchg   ax,ax
  0x000000000287fe4f: call   0x00000000028590a0  ; OopMap{off=52}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
                                                ;   {runtime_call}
  0x000000000287fe54: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
  0x000000000287fe55: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe8b]        # 0x000000000287fce8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@194 (line 92)
                                                ;   {section_word}
  0x000000000287fe5d: jmp    0x000000000287ff16
  0x000000000287fe62: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe86]        # 0x000000000287fcf0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@188 (line 90)
                                                ;   {section_word}
  0x000000000287fe6a: jmp    0x000000000287ff16
  0x000000000287fe6f: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe81]        # 0x000000000287fcf8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@182 (line 88)
                                                ;   {section_word}
  0x000000000287fe77: jmp    0x000000000287ff16
  0x000000000287fe7c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe7c]        # 0x000000000287fd00
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@176 (line 86)
                                                ;   {section_word}
  0x000000000287fe84: jmp    0x000000000287ff16
  0x000000000287fe89: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe77]        # 0x000000000287fd08
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@170 (line 84)
                                                ;   {section_word}
  0x000000000287fe91: jmp    0x000000000287ff16
  0x000000000287fe96: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe72]        # 0x000000000287fd10
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@164 (line 82)
                                                ;   {section_word}
  0x000000000287fe9e: jmp    0x000000000287ff16
  0x000000000287fea0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe70]        # 0x000000000287fd18
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@158 (line 80)
                                                ;   {section_word}
  0x000000000287fea8: jmp    0x000000000287ff16
  0x000000000287feaa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6e]        # 0x000000000287fd20
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@152 (line 78)
                                                ;   {section_word}
  0x000000000287feb2: jmp    0x000000000287ff16
  0x000000000287feb4: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe24]        # 0x000000000287fce0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@146 (line 76)
                                                ;   {section_word}
  0x000000000287febc: jmp    0x000000000287ff16
  0x000000000287febe: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6a]        # 0x000000000287fd30
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@140 (line 74)
                                                ;   {section_word}
  0x000000000287fec6: jmp    0x000000000287ff16
  0x000000000287fec8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe68]        # 0x000000000287fd38
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@134 (line 72)
                                                ;   {section_word}
  0x000000000287fed0: jmp    0x000000000287ff16
  0x000000000287fed2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe66]        # 0x000000000287fd40
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@128 (line 70)
                                                ;   {section_word}
  0x000000000287feda: jmp    0x000000000287ff16
  0x000000000287fedc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe64]        # 0x000000000287fd48
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@122 (line 68)
                                                ;   {section_word}
  0x000000000287fee4: jmp    0x000000000287ff16
  0x000000000287fee6: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe62]        # 0x000000000287fd50
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@116 (line 66)
                                                ;   {section_word}
  0x000000000287feee: jmp    0x000000000287ff16
  0x000000000287fef0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe60]        # 0x000000000287fd58
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@110 (line 64)
                                                ;   {section_word}
  0x000000000287fef8: jmp    0x000000000287ff16
  0x000000000287fefa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5e]        # 0x000000000287fd60
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@104 (line 62)
                                                ;   {section_word}
  0x000000000287ff02: jmp    0x000000000287ff16
  0x000000000287ff04: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5c]        # 0x000000000287fd68
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@98 (line 60)
                                                ;   {section_word}
  0x000000000287ff0c: jmp    0x000000000287ff16
  0x000000000287ff0e: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe12]        # 0x000000000287fd28
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x000000000287ff16: add    rsp,0x10
  0x000000000287ff1a: pop    rbp
  0x000000000287ff1b: test   DWORD PTR [rip+0xfffffffffd9b00df],eax        # 0x0000000000230000
                                                ;   {poll_return}
  0x000000000287ff21: ret    

และในที่สุดแอสเซมบลีที่มี 30 ราย (ด้านล่าง) จะมีลักษณะคล้ายกับ 18 รายยกเว้นส่วนเพิ่มเติมmovapd xmm0,xmm1ที่ปรากฏขึ้นตรงกลางของรหัสตามที่ @cHao เห็น - อย่างไรก็ตามเหตุผลที่น่าพึงพอใจที่สุดสำหรับการลดลงคือวิธีนี้ นานแล้วที่จะถูกแสดงด้วยการตั้งค่า JVM เริ่มต้น

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x0000000002524560: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x0000000002524567: push   rbp
  0x0000000002524568: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000252456c: movapd xmm1,xmm0
  0x0000000002524570: cmp    edx,0x1f
  0x0000000002524573: jae    0x0000000002524592  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524575: movsxd r10,edx
  0x0000000002524578: shl    r10,0x3
  0x000000000252457c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe3c]        # 0x00000000025243c0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@364 (line 118)
                                                ;   {section_word}
  0x0000000002524584: movabs r8,0x2524450       ;   {section_word}
  0x000000000252458e: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524592: mov    ebp,edx
  0x0000000002524594: mov    edx,0x31
  0x0000000002524599: xchg   ax,ax
  0x000000000252459b: call   0x00000000024f90a0  ; OopMap{off=64}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
                                                ;   {runtime_call}
  0x00000000025245a0: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
  0x00000000025245a1: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe27]        # 0x00000000025243d0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@358 (line 116)
                                                ;   {section_word}
  0x00000000025245a9: jmp    0x0000000002524744
  0x00000000025245ae: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe22]        # 0x00000000025243d8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@348 (line 114)
                                                ;   {section_word}
  0x00000000025245b6: jmp    0x0000000002524744
  0x00000000025245bb: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe1d]        # 0x00000000025243e0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@338 (line 112)
                                                ;   {section_word}
  0x00000000025245c3: jmp    0x0000000002524744
  0x00000000025245c8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe18]        # 0x00000000025243e8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@328 (line 110)
                                                ;   {section_word}
  0x00000000025245d0: jmp    0x0000000002524744
  0x00000000025245d5: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe13]        # 0x00000000025243f0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@318 (line 108)
                                                ;   {section_word}
  0x00000000025245dd: jmp    0x0000000002524744
  0x00000000025245e2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0e]        # 0x00000000025243f8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@308 (line 106)
                                                ;   {section_word}
  0x00000000025245ea: jmp    0x0000000002524744
  0x00000000025245ef: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe09]        # 0x0000000002524400
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@298 (line 104)
                                                ;   {section_word}
  0x00000000025245f7: jmp    0x0000000002524744
  0x00000000025245fc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe04]        # 0x0000000002524408
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@288 (line 102)
                                                ;   {section_word}
  0x0000000002524604: jmp    0x0000000002524744
  0x0000000002524609: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdff]        # 0x0000000002524410
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@278 (line 100)
                                                ;   {section_word}
  0x0000000002524611: jmp    0x0000000002524744
  0x0000000002524616: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdfa]        # 0x0000000002524418
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@268 (line 98)
                                                ;   {section_word}
  0x000000000252461e: jmp    0x0000000002524744
  0x0000000002524623: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffd9d]        # 0x00000000025243c8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@258 (line 96)
                                                ;   {section_word}
  0x000000000252462b: jmp    0x0000000002524744
  0x0000000002524630: movapd xmm0,xmm1
  0x0000000002524634: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0c]        # 0x0000000002524448
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@242 (line 92)
                                                ;   {section_word}
  0x000000000252463c: jmp    0x0000000002524744
  0x0000000002524641: movapd xmm0,xmm1
  0x0000000002524645: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffddb]        # 0x0000000002524428
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@236 (line 90)
                                                ;   {section_word}
  0x000000000252464d: jmp    0x0000000002524744
  0x0000000002524652: movapd xmm0,xmm1
  0x0000000002524656: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdd2]        # 0x0000000002524430
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@230 (line 88)
                                                ;   {section_word}
  0x000000000252465e: jmp    0x0000000002524744
  0x0000000002524663: movapd xmm0,xmm1
  0x0000000002524667: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdc9]        # 0x0000000002524438
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@224 (line 86)
                                                ;   {section_word}

[etc.]

  0x0000000002524744: add    rsp,0x10
  0x0000000002524748: pop    rbp
  0x0000000002524749: test   DWORD PTR [rip+0xfffffffffde1b8b1],eax        # 0x0000000000340000
                                                ;   {poll_return}
  0x000000000252474f: ret    

7
@ syb0rg บอกตามตรงฉันไม่เข้าใจรายละเอียดที่ดีเช่น ;-)
assylias

4
+1 สำหรับคำตอบที่ยอดเยี่ยม! คุณสามารถแยกชิ้นส่วนที่มี 30+ กรณีเพื่อเปรียบเทียบเมื่อประสิทธิภาพออกจาก "การจุ่ม" ในแผนภูมิของ OP หรือไม่
asteri


2
@AndrewBissell ฉันเดาว่าพฤติกรรมที่แตกต่างกันนั้นขึ้นอยู่กับ (i) การทดสอบประสิทธิภาพข้ามสถาปัตยกรรมซึ่งแสดงให้เห็นว่าอาร์เรย์ของพอยน์เตอร์นั้นมีประสิทธิภาพก็ต่อเมื่อจำนวนกรณีมากกว่า 18 หรือ (ii) โค้ดถูกทำโปรไฟล์เป็น มันถูกเรียกใช้และผู้สร้างโปรไฟล์กำหนดวิธีการที่ดีกว่าในระหว่างรันไทม์ ฉันไม่พบคำตอบ
assylias

3
การถอดแยกชิ้นส่วน 30 ตัวและตัวเรือน 18 ตัวส่วนใหญ่จะเหมือนกัน ความแตกต่างนั้นดูเหมือนจะ จำกัด อยู่ที่การเพิ่มการลงทะเบียนเพิ่มเติมเล็กน้อยหลังจากกรณีที่ 11 ไม่สามารถบอกได้ว่าทำไม JITter ถึงทำเช่นนั้น ดูเหมือนว่าไม่จำเป็น
cHao

46

Switch - case เร็วขึ้นหากค่า case ถูกวางในช่วงแคบเช่น

case 1:
case 2:
case 3:
..
..
case n:

เพราะในกรณีนี้คอมไพเลอร์สามารถหลีกเลี่ยงการทำการเปรียบเทียบสำหรับทุกกรณีของเลกในคำสั่งสลับ คอมไพเลอร์สร้างตารางกระโดดซึ่งมีที่อยู่ของการกระทำที่จะต้องดำเนินการบนขาที่แตกต่างกัน jump tableค่าที่สวิทช์จะถูกดำเนินการมีการจัดการเพื่อแปลงเป็นดัชนีในการที่จะ ในการใช้งานนี้เวลาที่ใช้ในคำสั่ง switch น้อยกว่าเวลาที่ใช้ในคำสั่ง if-else-if ที่เทียบเท่ากันมาก นอกจากนี้เวลาที่ใช้ในคำสั่ง switch นั้นไม่ขึ้นอยู่กับจำนวนของขาเคสในคำสั่ง switch

ตามที่ระบุในวิกิพีเดียเกี่ยวกับคำสั่งเปลี่ยนในส่วนการรวบรวม

หากช่วงของค่าอินพุตระบุได้ว่า 'เล็ก' และมีช่องว่างเพียงไม่กี่ตัวคอมไพเลอร์บางตัวที่รวมเครื่องมือเพิ่มประสิทธิภาพอาจใช้คำสั่งสวิตช์เป็นตารางสาขาหรืออาร์เรย์ของตัวชี้ฟังก์ชันดัชนีที่จัดทำขึ้นแทนชุดคำสั่งแบบยาว สิ่งนี้ทำให้คำสั่ง switch สามารถกำหนดได้ทันทีว่าสาขาใดที่จะดำเนินการโดยไม่ต้องผ่านรายการเปรียบเทียบ


4
ไม่ถูกต้อง มันจะเร็วขึ้นโดยไม่คำนึงถึงค่าเคสที่แคบหรือกว้างในช่วง มันเป็น O (1) - ไม่สำคัญว่าค่าตัวพิมพ์เล็กและใหญ่จะแตกต่างกันอย่างไร
Aniket Inge

6
@Aniket: อ่านบทความของวิกิพีเดียนี้ en.wikipedia.org/wiki/Branch_table
Vishal K

14
@Aniket: ไม่ใช่ O (1) ถ้าช่วงกว้างและห่าง มีสวิตช์สองชนิดและถ้าช่วงนั้นแผ่ออกมากเกินไป Java จะรวบรวมมันเป็น "lookupswitch" แทนที่จะเป็น "tableswitch" อดีตต้องการการเปรียบเทียบต่อสาขาจนกว่าจะพบหนึ่งในขณะที่หลังไม่พบ
cHao

4
Wikipedia เป็นสถานที่ที่เหมาะสมในการค้นหาแหล่งข้อมูล แต่ไม่ควรพิจารณาว่าเป็นแหล่งข้อมูลที่เชื่อถือได้ สิ่งที่คุณอ่านมีข้อมูลมือสองที่ดีที่สุด
cHao

6
@Aniket: ในความเป็นธรรมทั้งหมดการถอดแยกชิ้นส่วนนั้นขึ้นอยู่กับ JVM ที่กำหนดบนแพลตฟอร์มเฉพาะ คนอื่นอาจแปลมันต่างกัน ในความเป็นจริงบางคนอาจใช้ตารางแฮชสำหรับการค้นหาสลับ มันยังคงทำงานได้ไม่ดีเหมือนโต๊ะบนโต๊ะ แต่อย่างน้อยก็สามารถปิดได้ มันใช้เวลานานกว่ากับ JIT และเกี่ยวข้องกับการใช้อัลกอริทึมการแปลงแป้นพิมพ์กับอินพุต ดังนั้นแม้ว่ารหัสแอสเซมบลีที่เกิดขึ้นสามารถ enlightening ก็ไม่ได้มีอำนาจอย่างใดอย่างหนึ่งเว้นแต่ว่าคุณกำลังพูดถึงเฉพาะฮอตสปอต v1.7 สิ่งที่อยู่บน Windows x86_64
cHao

30

คำตอบอยู่ในไบต์:

SwitchTest10.java

public class SwitchTest10 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 10: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

ไบต์ที่สอดคล้องกัน; แสดงเฉพาะส่วนที่เกี่ยวข้อง:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 10
        0: 60;
        1: 70;
        2: 80;
        3: 90;
        4: 100;
        5: 110;
        6: 120;
        7: 131;
        8: 142;
        9: 153;
        10: 164;
        default: 175 }

SwitchTest22.java:

public class SwitchTest22 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 100: System.out.println(10);
                    break;

            case 110: System.out.println(10);
                    break;
            case 120: System.out.println(10);
                    break;
            case 130: System.out.println(10);
                    break;
            case 140: System.out.println(10);
                    break;
            case 150: System.out.println(10);
                    break;
            case 160: System.out.println(10);
                    break;
            case 170: System.out.println(10);
                    break;
            case 180: System.out.println(10);
                    break;
            case 190: System.out.println(10);
                    break;
            case 200: System.out.println(10);
                    break;
            case 210: System.out.println(10);
                    break;

            case 220: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

ไบต์ที่สอดคล้องกัน; อีกครั้งแสดงเฉพาะส่วนที่เกี่ยวข้อง:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   lookupswitch{ //23
        0: 196;
        1: 206;
        2: 216;
        3: 226;
        4: 236;
        5: 246;
        6: 256;
        7: 267;
        8: 278;
        9: 289;
        100: 300;
        110: 311;
        120: 322;
        130: 333;
        140: 344;
        150: 355;
        160: 366;
        170: 377;
        180: 388;
        190: 399;
        200: 410;
        210: 421;
        220: 432;
        default: 443 }

ในกรณีแรกที่มีช่วงแคบ bytecode tableswitchรวบรวมใช้ ในกรณีที่สอง bytecode lookupswitchรวบรวมใช้

ในtableswitchค่าจำนวนเต็มที่ด้านบนสุดของสแต็กจะใช้เพื่อทำดัชนีลงในตารางเพื่อค้นหาเป้าหมายสาขา / กระโดด การกระโดด / สาขานี้จะดำเนินการทันที ดังนั้นนี่คือการO(1)ดำเนินการ

A lookupswitchซับซ้อนกว่า ในกรณีนี้ค่าจำนวนเต็มจะต้องเปรียบเทียบกับคีย์ทั้งหมดในตารางจนกว่าจะพบคีย์ที่ถูกต้อง หลังจากพบคีย์แล้วจะมีการใช้กิ่ง / กระโดดเป้าหมาย (ที่คีย์นี้ถูกแมปไป) เพื่อใช้สำหรับการกระโดด ตารางที่ใช้ในการlookupswitchเรียงลำดับและอัลกอริทึมการค้นหาแบบไบนารีสามารถใช้เพื่อค้นหาคีย์ที่ถูกต้อง ผลการดำเนินงานสำหรับการค้นหาไบนารีO(log n)และกระบวนการทั้งหมดยังเป็นเพราะกระโดดยังคงเป็นO(log n) O(1)ดังนั้นเหตุผลที่ทำให้ประสิทธิภาพลดลงในกรณีของช่วงที่กระจัดกระจายนั่นคือต้องค้นหาคีย์ที่ถูกต้องก่อนเนื่องจากคุณไม่สามารถสร้างดัชนีลงในตารางได้โดยตรง

หากมีค่ากระจัดกระจายและคุณมีเพียงการtableswitchใช้งานตารางจะประกอบด้วยรายการจำลองที่ชี้ไปที่defaultตัวเลือก ตัวอย่างเช่นสมมติว่ารายการสุดท้ายในแทนที่จะSwitchTest10.javaเป็นคุณจะได้รับ:2110

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 21
        0: 104;
        1: 114;
        2: 124;
        3: 134;
        4: 144;
        5: 154;
        6: 164;
        7: 175;
        8: 186;
        9: 197;
        10: 219;
        11: 219;
        12: 219;
        13: 219;
        14: 219;
        15: 219;
        16: 219;
        17: 219;
        18: 219;
        19: 219;
        20: 219;
        21: 208;
        default: 219 }

ดังนั้นคอมไพเลอร์จึงสร้างตารางขนาดใหญ่นี้ที่มีรายการจำลองระหว่างช่องว่างชี้ไปที่เป้าหมายสาขาของdefaultคำสั่ง แม้ว่าจะไม่มี a defaultแต่ก็จะมีรายการที่ชี้ไปยังคำแนะนำหลังจากบล็อกสวิตช์ ฉันไม่ได้ทดสอบพื้นฐานบางอย่างและฉันพบว่าถ้าช่องว่างระหว่างดัชนีที่แล้วและก่อนหน้านี้หนึ่ง (คน9) มีค่ามากกว่า35จะใช้แทนlookupswitchtableswitch

พฤติกรรมของswitchคำสั่งถูกกำหนดในJava Virtual Machine Specification (§3.10) :

ในกรณีที่สวิทช์กระจัดกระจายการแสดงตารางของคำสั่ง tableswitch จะไม่มีประสิทธิภาพในแง่ของพื้นที่ คำสั่ง lookupswitch อาจถูกนำมาใช้แทน คำสั่ง lookupswitch จับคู่คีย์ int (ค่าของเลเบล case) โดยมี offsets เป้าหมายในตาราง เมื่อคำสั่ง lookupswitch ถูกดำเนินการค่าของนิพจน์ของสวิตช์จะถูกเปรียบเทียบกับคีย์ในตาราง หากหนึ่งในคีย์ที่ตรงกับค่าของการแสดงออกการดำเนินการต่อไปที่เป้าหมายชดเชยที่เกี่ยวข้อง หากไม่มีคีย์ที่ตรงกันการดำเนินการจะดำเนินต่อที่เป้าหมายเริ่มต้น [ ... ]


1
ฉันเข้าใจจากคำถามที่ว่าตัวเลขนั้นต่อเนื่องกันตลอด แต่ช่วงนั้นยาวหรือยาวมากขึ้น - นั่นคือในตัวอย่างหนึ่งกรณีไปจาก 0 ถึง 5 ในขณะที่อีกตัวอย่างหนึ่งพวกเขาเปลี่ยนจาก 0 ถึง 30 - และไม่มีตัวอย่างใดที่ใช้ค่าเบาบาง
assylias

@assylias อืมน่าสนใจ ฉันเดาว่าฉันเข้าใจผิดคำถาม ขอผมทดลองอีกหน่อย คุณกำลังบอกว่าแม้จะมีช่วงต่อเนื่องตั้งแต่ 0-30 คอมไพเลอร์ก็ใช้lookupswitch?
Vivin Paliath

@ VivinPaliath: ใช่ในการทดสอบของฉันกรณีค่าคงที่จะต่อเนื่องกันอยู่เสมอดังนั้นฉันจึงทำการทดสอบสวิตช์บน [0, 1], [0, 1, 2], [0, 1, 2, 3] ... ฯลฯ
Andrew Bissell

@VivinPaliath ไม่ bytecode มักใช้ tableswitch - อย่างไรก็ตามคอมไพเลอร์ JIT ดูเหมือนจะไม่รวบรวม tableswitch เพื่อประกอบในลักษณะเดียวกันขึ้นอยู่กับจำนวนรายการที่มี
assylias

6
@VivinPaliath ฉันสามารถถามคำถามได้ชัดเจนขึ้นอย่างแน่นอน ฉันแยกแยะความลึกของฉันเมื่อมันมาถึงการประเมินคำตอบที่เกี่ยวข้องกับสิ่งที่ bytecode & ชุดประกอบระดับต่ำนี้ ดูเหมือนว่าฉันจะชอบความแตกต่างของ tableswitch / lookupswitch จริง ๆ แล้วที่นี่สำคัญและคุณเป็นคำตอบเดียวที่ใช้คำศัพท์เหล่านั้นจนถึงตอนนี้ (แม้ว่าคนอื่น ๆ อาจตั้งแนวคิดเดียวกันกับคำศัพท์ที่แตกต่างกัน) นอกจากนี้ฉันชอบมีลิงก์ JVM Spec ด้วย
Andrew Bissell

19

เนื่องจากคำถามได้รับการตอบแล้ว (มากกว่าหรือน้อยกว่า) นี่คือเคล็ดลับ ใช้

private static final double[] mul={1d, 10d...};
static double multiplyByPowerOfTen(final double d, final int exponent) {
      if (exponent<0 || exponent>=mul.length) throw new ParseException();//or just leave the IOOBE be
      return mul[exponent]*d;
}

รหัสนั้นใช้ IC น้อยกว่าอย่างมาก (แคชคำสั่ง) และจะถูกแทรกไว้ อาร์เรย์จะอยู่ในแคชข้อมูล L1 หากรหัสร้อน ตารางการค้นหามักจะชนะเสมอ (โดยเฉพาะอย่างยิ่งใน microbenchmarks: D)

แก้ไข: หากคุณต้องการวิธีที่จะร้อน - อินไลน์พิจารณาเส้นทางที่ไม่เร็วอย่างthrow new ParseException()ที่จะเป็นที่สั้นที่สุดหรือย้ายพวกเขาเพื่อแยกวิธีคงที่ (จึงทำให้พวกเขาสั้นเป็นขั้นต่ำ) นั่นคือthrow new ParseException("Unhandled power of ten " + power, 0);ความคิดที่อ่อนแอ b / c มันกินงบประมาณ inlining จำนวนมากสำหรับโค้ดที่สามารถตีความได้เพียงอย่างเดียว - การต่อข้อมูลสตริงนั้นค่อนข้างละเอียดในรหัสไบต์ ข้อมูลเพิ่มเติมและกรณีจริงด้วย ArrayList

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