มาโครที่น่าจะเป็น / ไม่น่าจะทำงานในเคอร์เนล Linux ได้อย่างไรและประโยชน์ของมันคืออะไร?


349

ฉันขุดผ่านบางส่วนของเคอร์เนล Linux และพบสายเช่นนี้:

if (unlikely(fd < 0))
{
    /* Do something */
}

หรือ

if (likely(!err))
{
    /* Do something */
}

ฉันได้พบคำจำกัดความของพวกเขา:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

ฉันรู้ว่ามีไว้สำหรับการปรับให้เหมาะสม แต่จะทำงานอย่างไร และสามารถลดขนาดประสิทธิภาพ / ขนาดได้เท่าไรจากการใช้งาน? และมันก็คุ้มค่ากับความยุ่งยาก (และการสูญเสียความสามารถในการพกพา) อย่างน้อยในโค้ดคอขวด (ใน userspace แน่นอน)


7
สิ่งนี้ไม่เฉพาะเจาะจงกับเคอร์เนล Linux หรือเกี่ยวกับมาโคร แต่เป็นการเพิ่มประสิทธิภาพของคอมไพเลอร์ สิ่งนี้ควรถูก retagged เพื่อสะท้อนสิ่งนั้นหรือไม่
Cody Brocious


2
ดูเพิ่มเติมBOOST_LIKELY
Ruggero Turra

4
ที่เกี่ยวข้อง: มาตรฐานในการใช้__builtin_expectกับคำถามอื่น
YSC

13
ไม่มีปัญหาการพกพา คุณสามารถทำสิ่งต่าง ๆ เช่น#define likely(x) (x)และ#define unlikely(x) (x)บนแพลตฟอร์มที่ไม่สนับสนุนการบอกใบ้เช่นนี้
David Schwartz

คำตอบ:


329

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

เช่นเดียวกับการปรับปรุงประสิทธิภาพดังกล่าวทั้งหมดคุณควรทำหลังจากการทำโปรไฟล์อย่างละเอียดเพื่อให้แน่ใจว่ารหัสอยู่ในคอขวดและอาจได้รับลักษณะไมโครซึ่งจะถูกเรียกใช้ในการวนรอบอย่างแน่นหนา โดยทั่วไปแล้วนักพัฒนา Linux นั้นค่อนข้างมีประสบการณ์ดังนั้นฉันจึงนึกภาพว่าพวกเขาจะทำเช่นนั้น พวกเขาไม่สนใจเกี่ยวกับการพกพามากนักเนื่องจากพวกเขาตั้งเป้าหมายที่ gcc เท่านั้นและพวกเขามีความคิดที่ใกล้ชิดมากเกี่ยวกับการประกอบที่พวกเขาต้องการให้มันสร้างขึ้น


3
มาโครเหล่านี้ส่วนใหญ่ใช้สำหรับการตรวจสอบข้อผิดพลาด เนื่องจากข้อผิดพลาดทำให้การดำเนินงานปกติลดน้อยลง มีเพียงไม่กี่คนที่ทำโปรไฟล์หรือคำนวณเพื่อตัดสินใจเลือกใบไม้ที่ใช้มากที่สุด ...
gavenkoa

51
ในส่วนที่เกี่ยวกับ"[...]that it is being run in a tight loop"ซีพียูจำนวนมากมีตัวทำนายสาขาดังนั้นการใช้มาโครเหล่านี้จะช่วยให้เรียกใช้รหัสเวลาครั้งแรกเท่านั้นหรือเมื่อตารางประวัติถูกเขียนทับโดยสาขาอื่นที่มีดัชนีเดียวกันลงในตารางการแยกสาขา ในการวนรอบที่แน่นหนาและสมมติว่าสาขาไปทางเดียวส่วนใหญ่ตัวพยากรณ์สาขาจะเริ่มคาดเดาสาขาที่ถูกต้องอย่างรวดเร็ว - เพื่อนของคุณอวดรู้
Ross Rogers

8
@RossRogers: สิ่งที่เกิดขึ้นจริงๆคือคอมไพเลอร์จัดเรียงสาขาดังนั้นกรณีที่พบบ่อยคือสิ่งที่ไม่ต้องดำเนินการ สิ่งนี้เร็วขึ้นแม้ว่าการคาดคะเนสาขาจะทำงานได้ สาขาที่ถ่ายเป็นปัญหาสำหรับการดึงคำแนะนำและถอดรหัสแม้ว่าจะทำนายได้อย่างสมบูรณ์ก็ตาม ซีพียูบางตัวจะทำนายกิ่งที่ไม่ได้อยู่ในตารางประวัติแบบคงที่โดยทั่วไปแล้วจะถือว่าไม่ถูกนำไปใช้สำหรับสาขาย่อย Intel CPUs ใช้งานไม่ได้: พวกเขาไม่ลองตรวจสอบว่ารายการตารางตัวทำนายสำหรับสาขานี้พวกเขาเพียงแค่ใช้มันอยู่แล้ว สาขาร้อนและอาจสาขาเย็นนามแฝงรายการเดียวกัน ...
ปีเตอร์ Cordes

12
คำตอบนี้ส่วนใหญ่ล้าสมัยเนื่องจากการอ้างสิทธิ์หลักคือช่วยในการคาดคะเนสาขาและตามที่ @PeterCordes ชี้ให้เห็นในฮาร์ดแวร์ที่ทันสมัยส่วนใหญ่ไม่มีการทำนายโดยปริยายหรือแบบคงที่สาขาที่ชัดเจน ในความเป็นจริงแล้วคอมไพเลอร์ใช้คำแนะนำเพื่อปรับโค้ดให้เหมาะสมไม่ว่าจะเกี่ยวข้องกับคำใบ้สาขาคงที่หรือการเพิ่มประสิทธิภาพประเภทอื่น ๆ สำหรับสถาปัตยกรรมส่วนใหญ่ในวันนี้มันเป็น "การเพิ่มประสิทธิภาพอื่น ๆ " ที่มีความสำคัญเช่นทำให้เส้นทางร้อนต่อเนื่องจัดตารางเส้นทางร้อนได้ดีขึ้นลดขนาดของเส้นทางที่ช้าลดขนาดเวกเตอร์เฉพาะเส้นทางที่คาดหวัง ฯลฯ
BeeOnRope

3
@BeeOnRope เนื่องจากการดึงข้อมูลล่วงหน้าของแคชและขนาดคำยังมีข้อได้เปรียบในการใช้งานโปรแกรมเป็นแนวตรง ตำแหน่งหน่วยความจำถัดไปจะถูกดึงออกมาแล้วและในแคชสาขาเป้าหมายอาจจะมีหรือไม่มีก็ได้ ด้วยซีพียู 64 บิตคุณสามารถคว้าได้อย่างน้อย 64 บิตในแต่ละครั้ง ขึ้นอยู่กับ interleave ของ DRAM มันอาจจะเป็น 2x 2x หรือมากกว่านั้นที่ถูกจับ
ไบรซ์

88

ลองถอดรหัสเพื่อดูว่า GCC 4.8 ทำอะไรกับมัน

ไม่มี __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        printf("%d\n", i);
    puts("a");
    return 0;
}

รวบรวมและถอดรหัสกับ GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

เอาท์พุท:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

ลำดับการเรียนการสอนในหน่วยความจำไม่เปลี่ยนแปลง: ก่อนprintfแล้วputsและretqกลับ

กับ __builtin_expect

ตอนนี้แทนที่if (i)ด้วย:

if (__builtin_expect(i, 0))

และเราได้รับ:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

printf(รวบรวม__printf_chk) ถูกย้ายไปยังส่วนท้ายสุดของฟังก์ชั่นหลังจากputsและผลตอบแทนในการปรับปรุงการทำนายสาขาดังกล่าวโดยคำตอบอื่น ๆ

ดังนั้นมันจึงเหมือนกับ:

int main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

-O0การเพิ่มประสิทธิภาพนี้ไม่ได้ทำด้วย

แต่โชคดีในการเขียนตัวอย่างที่ทำงานได้เร็ว__builtin_expectกว่าโดยไม่ต้องใช้ CPU ในวันนี้ ความพยายามที่ไร้เดียงสาของฉันอยู่ที่นี่

C ++ 20 [[likely]]และ[[unlikely]]

C ++ 20 ได้สร้างมาตรฐานในตัว C ++ เหล่านั้น: วิธีการใช้คุณลักษณะที่เป็นไปได้ / ไม่น่าเป็นไปได้ของ C ++ 20 ในคำสั่ง if-elseพวกเขามีแนวโน้มที่จะ (เล่นสำนวน!) ทำสิ่งเดียวกัน


71

นี่คือมาโครที่ให้คำแนะนำแก่คอมไพเลอร์เกี่ยวกับวิธีที่สาขาอาจไป มาโครจะขยายไปยังส่วนขยายเฉพาะของ GCC หากมีให้บริการ

GCC ใช้สิ่งเหล่านี้เพื่อเพิ่มประสิทธิภาพสำหรับการทำนายสาขา ตัวอย่างเช่นหากคุณมีสิ่งที่ชอบดังต่อไปนี้

if (unlikely(x)) {
  dosomething();
}

return x;

จากนั้นมันสามารถปรับโครงสร้างโค้ดนี้ให้เป็นอะไรที่มากกว่า:

if (!x) {
  return x;
}

dosomething();
return x;

ประโยชน์ของการทำเช่นนี้คือเมื่อตัวประมวลผลใช้สาขาเป็นครั้งแรกจะมีค่าใช้จ่ายที่สำคัญเนื่องจากอาจได้รับการโหลดและเรียกใช้โค้ดจากข้างหน้าอย่างพิเศษ เมื่อมันกำหนดว่าจะใช้สาขาแล้วมันจะต้องทำให้เป็นโมฆะและเริ่มต้นที่เป้าหมายสาขา

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

มีกลยุทธ์อื่น ๆ อีกมากมายที่คอมไพเลอร์และโปรเซสเซอร์สามารถใช้ในสถานการณ์เหล่านี้ คุณสามารถดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการทำงานของตัวพยากรณ์สาขาที่ Wikipedia: http://en.wikipedia.org/wiki/Branch_predictor


3
นอกจากนี้ยังส่งผลกระทบต่อรอยเท้า icache - โดยการเก็บโค้ดขนาดเล็กออกจากเส้นทางร้อน
fche

2
แม่นยำยิ่งขึ้นสามารถทำได้ด้วยgotos โดยไม่ต้องทำซ้ำreturn x: stackoverflow.com/a/31133787/895245
Ciro Santilli 郝海东冠状病病六四事件法轮功

7

พวกเขาทำให้คอมไพเลอร์ปล่อยคำแนะนำสาขาที่เหมาะสมที่ฮาร์ดแวร์สนับสนุนพวกเขา นี่มักจะหมายถึงการ twiddling สองสามบิตใน opcode คำสั่งดังนั้นขนาดรหัสจะไม่เปลี่ยนแปลง ซีพียูจะเริ่มดึงข้อมูลคำแนะนำจากตำแหน่งที่คาดการณ์ไว้และล้างข้อมูลไปป์ไลน์และเริ่มต้นใหม่หากพบว่าผิดพลาดเมื่อถึงสาขา ในกรณีที่คำใบ้ถูกต้องสิ่งนี้จะทำให้สาขาเร็วขึ้น - แม่นยำขึ้นอยู่กับฮาร์ดแวร์ และจะมีผลต่อประสิทธิภาพของรหัสมากน้อยเพียงใดขึ้นอยู่กับสัดส่วนของคำใบ้เวลาที่ถูกต้อง

ยกตัวอย่างเช่นบน PowerPC CPU สาขาที่ไม่ได้คาดไว้อาจใช้เวลา 16 รอบหนึ่งใน 8 ที่ถูกแบะท่าอย่างถูกต้องและ 1 ที่ 24 ไม่ถูกต้อง

การพกพาไม่ได้เป็นปัญหาจริงๆ - น่าจะเป็นความหมายที่อยู่ในส่วนหัวต่อแพลตฟอร์ม; คุณสามารถนิยาม "น่าจะเป็น" และ "ไม่น่าเป็นไปได้" สำหรับแพลตฟอร์มที่ไม่สนับสนุนคำใบ้สาขาคงที่


3
สำหรับเรกคอร์ด x86 ใช้พื้นที่เพิ่มเติมสำหรับคำแนะนำสาขา คุณต้องมีส่วนนำหน้าหนึ่งไบต์ในสาขาเพื่อระบุคำใบ้ที่เหมาะสม เห็นด้วยว่าคำใบ้นั้นเป็นสิ่งที่ดี (TM)
Cody Brocious

2
แดง CISC ซีพียูและคำแนะนำตัวแปรความยาวของพวกเขา;)
Moonshadow

3
ซีพียู Dang RISC - อยู่ห่างจากคำแนะนำ 15 ไบต์ของฉัน;)
Cody Brocious

7
@CodyBrocious: การแนะนำสาขาได้รับการแนะนำด้วย P4 แต่ถูกละทิ้งพร้อมกับ P4 ซีพียู x86 อื่น ๆ ทั้งหมดไม่สนใจคำนำหน้านั้น (เพราะคำนำหน้านั้นจะถูกละเว้นในบริบทที่ไม่มีความหมายเสมอ) มาโครเหล่านี้ไม่ทำให้ gcc เปล่งคำนำหน้าคำใบ้สาขาใน x86 พวกมันช่วยให้คุณได้รับ gcc เพื่อจัดโครงสร้างการทำงานของคุณด้วยกิ่งไม้ที่ถูกยึดน้อยลงบนทางลัด
Peter Cordes

5
long __builtin_expect(long EXP, long C);

โครงสร้างนี้บอกคอมไพเลอร์ว่านิพจน์ EXP ส่วนใหญ่จะมีค่า C ค่าส่งคืนคือ EXP __builtin_expectมีวัตถุประสงค์เพื่อใช้ในการแสดงออกตามเงื่อนไข ในเกือบทุกกรณีจะใช้ในบริบทของนิพจน์บูลีนซึ่งในกรณีนี้จะสะดวกกว่าในการกำหนดมาโครตัวช่วยสองตัว:

#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

สามารถใช้มาโครเหล่านี้ได้เช่นเดียวกับใน

if (likely(a > 1))

การอ้างอิง: https://www.akkadia.org/drepper/cpumemory.pdf


1
ตามที่ถูกถามในความคิดเห็นต่อคำตอบอื่น - อะไรคือสาเหตุของการผกผันสองครั้งในมาโคร (เช่นทำไมต้องใช้__builtin_expect(!!(expr),0)แทนที่จะเป็นแค่__builtin_expect((expr),0)?
Michael Firth

1
@MichaelFirth "รักร่วมเพศคู่" เทียบเท่ากับการหล่อบางสิ่งบางอย่างไป!! boolบางคนชอบเขียนด้วยวิธีนี้
Ben XO

2

(ความคิดเห็นทั่วไป - คำตอบอื่น ๆ ครอบคลุมรายละเอียด)

ไม่มีเหตุผลที่คุณควรสูญเสียการพกพาโดยใช้พวกเขา

คุณมีตัวเลือกในการสร้าง nil-effect "inline" หรือมาโครแบบง่าย ๆ ที่จะช่วยให้คุณสามารถคอมไพล์บนแพลตฟอร์มอื่นด้วยคอมไพเลอร์อื่น ๆ

คุณจะไม่ได้รับประโยชน์จากการปรับให้เหมาะสมหากคุณใช้แพลตฟอร์มอื่น


1
คุณไม่ใช้การพกพา - แพลตฟอร์มที่ไม่รองรับเพียงแค่กำหนดให้ขยายเป็นสตริงว่าง
sharptooth

2
ฉันคิดว่าคุณสองคนเห็นด้วยกันจริง ๆ - มันเป็นเพียงถ้อยคำที่สับสน (จากรูปลักษณ์ของมันความคิดเห็นของแอนดรูว์พูดว่า "คุณสามารถใช้มันได้โดยไม่สูญเสียการพกพา" แต่ชาร์ปคิดว่าเขาพูดว่า "อย่าใช้มันเพราะพกพาไม่ได้" และคัดค้าน)
Miral

2

ตามความเห็นของCodyสิ่งนี้ไม่เกี่ยวข้องกับ Linux แต่เป็นคำใบ้ของคอมไพเลอร์ สิ่งที่เกิดขึ้นจะขึ้นอยู่กับเวอร์ชั่นของสถาปัตยกรรมและคอมไพเลอร์

ฟีเจอร์นี้โดยเฉพาะใน Linux นั้นค่อนข้างจะใช้ผิดในไดรเวอร์ ในฐานะที่เป็นosgxชี้ให้เห็นในความหมายของคุณลักษณะร้อนใด ๆhotหรือcoldฟังก์ชั่นที่เรียกว่าด้วยในบล็อกสามารถบอกเป็นนัยว่าสภาพมีแนวโน้มหรือไม่ ตัวอย่างเช่นdump_stack()มีการทำเครื่องหมายcoldเพื่อให้ซ้ำซ้อน

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

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


2

ในรุ่นลินุกซ์หลายรุ่นคุณสามารถค้นหา complier.h ใน / usr / linux / คุณสามารถรวมมันเพื่อใช้งานได้ง่าย และความเห็นอื่นไม่น่าเป็นไปได้ () มีประโยชน์มากกว่าน่าจะเป็น () เพราะ

if ( likely( ... ) ) {
     doSomething();
}

มันสามารถปรับให้เหมาะสมเช่นกันในคอมไพเลอร์จำนวนมาก

และโดยวิธีการถ้าคุณต้องการสังเกตพฤติกรรมรายละเอียดของรหัสคุณสามารถทำได้ดังนี้:

gcc -c test.c objdump -d test.o> obj.s

จากนั้นเปิด obj.s คุณจะพบคำตอบ


1

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

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


10
gcc ไม่เคยสร้างคำแนะนำสาขา x86 - อย่างน้อยซีพียูทั้งหมดของ Intel จะไม่สนใจก็ตาม มันจะพยายาม จำกัด ขนาดของรหัสในพื้นที่ที่ไม่น่าเป็นไปได้ด้วยการหลีกเลี่ยงการอินไลน์และวนลูปที่ยังไม่เปิด
alex แปลกที่

1

นี่คือฟังก์ชั่น GCC สำหรับโปรแกรมเมอร์เพื่อบอกใบ้แก่คอมไพเลอร์เกี่ยวกับเงื่อนไขของสาขาที่เป็นไปได้มากที่สุดในนิพจน์ที่กำหนด สิ่งนี้อนุญาตให้คอมไพเลอร์สร้างคำสั่งสาขาเพื่อให้กรณีที่พบบ่อยที่สุดใช้จำนวนคำสั่งน้อยที่สุดในการดำเนินการ

วิธีการสร้างคำสั่งสาขาขึ้นอยู่กับสถาปัตยกรรมของโปรเซสเซอร์

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