เกี่ยวกับการประมาณที่เร็วขึ้นของบันทึก (x)


10

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

const double ee = exp(1);

double series_ln_taylor(double n){ /* n = e^a * b, where a is an non-negative integer */
    double lgVal = 0, term, now;
    int i, flag = 1;

    if ( n <= 0 ) return 1e-300;
    if ( n * ee < 1 )
        n = 1.0 / n, flag = -1; /* for extremely small n, use e^-x = 1/n */

    for ( term = 1; term < n ; term *= ee, lgVal++ );
    n /= term;

    /* log(1 - x) = -x - x**2/2 - x**3/3... */
    n = 1 - n;
    now = term = n;
    for ( i = 1 ; ; ){
        lgVal -= now;
        term *= n;
        now = term / ++i;
        if ( now < 1e-17 ) break;
    }

    if ( flag == -1 ) lgVal = -lgVal;

    return lgVal;
}

นี่ฉันกำลังพยายามที่จะหาเพื่อให้อีเป็นเพียงกว่า n และจากนั้นผมเพิ่มค่าลอการิทึมของnaeaซึ่งน้อยกว่า 1 ณ จุดนี้การขยายตัวของเทย์เลอร์Loกรัม(1-x)สามารถนำมาใช้โดยไม่ต้องกังวลnealog(1  x)

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

ฟังก์ชันมาพร้อมกับไลบรารีมาตรฐาน C นั้นเร็วกว่าการติดตั้งนี้เกือบ 5.1 เท่าlog(x)

อัปเดต 1 : การใช้ซีรีย์ไฮเปอร์โบลิกอาร์แทนที่กล่าวถึงในวิกิพีเดียการคำนวณดูเหมือนว่าจะช้ากว่า 2.2 เท่ากว่าฟังก์ชั่นบันทึกในไลบรารีมาตรฐาน C แม้ว่าฉันจะไม่ได้ตรวจสอบประสิทธิภาพอย่างกว้างขวางและสำหรับจำนวนที่มากขึ้นการใช้งานในปัจจุบันของฉันดูเหมือนจะช้ามากจริงๆ ฉันต้องการตรวจสอบการติดตั้งทั้งข้อผิดพลาดและเวลาเฉลี่ยสำหรับหมายเลขที่หลากหลายถ้าฉันสามารถจัดการได้ นี่คือความพยายามครั้งที่สองของฉัน

double series_ln_arctanh(double n){ /* n = e^a * b, where a is an non-negative integer */
    double lgVal = 0, term, now, sm;
    int i, flag = 1;

    if ( n <= 0 ) return 1e-300;
    if ( n * ee < 1 ) n = 1.0 / n, flag = -1; /* for extremely small n, use e^-x = 1/n */

    for ( term = 1; term < n ; term *= ee, lgVal++ );
    n /= term;

    /* log(x) = 2 arctanh((x-1)/(x+1)) */
    n = (1 - n)/(n + 1);

    now = term = n;
    n *= n;
    sm = 0;
    for ( i = 3 ; ; i += 2 ){
        sm += now;
        term *= n;
        now = term / i;
       if ( now < 1e-17 ) break;
    }

    lgVal -= 2*sm;

    if ( flag == -1 ) lgVal = -lgVal;
    return lgVal;
}

ข้อเสนอแนะหรือคำวิจารณ์ใด ๆ ที่ชื่นชม

อัปเดต 2:ตามคำแนะนำที่ทำไว้ด้านล่างฉันได้เพิ่มการเปลี่ยนแปลงที่เพิ่มขึ้นที่นี่ซึ่งช้ากว่าการใช้ไลบรารีมาตรฐานประมาณ 2.5 เท่า อย่างไรก็ตามฉันได้ทดสอบเฉพาะจำนวนเต็มในครั้งนี้สำหรับจำนวนที่มากขึ้นรันไทม์จะเพิ่มขึ้น สำหรับตอนนี้. ฉันยังไม่รู้เทคนิคในการสร้างตัวเลขสองตัวแบบสุ่ม1 e 308ดังนั้นจึงยังไม่ได้ทำการทดสอบอย่างสมบูรณ์ เพื่อให้รหัสมีเสถียรภาพมากขึ้นฉันได้เพิ่มการแก้ไขสำหรับตัวพิมพ์มุม ข้อผิดพลาดเฉลี่ยสำหรับการทดสอบที่ผมทำอยู่ที่ประมาณ4 E - 151e81e3084e15

double series_ln_better(double n){ /* n = e^a * b, where a is an non-negative integer */
    double lgVal = 0, term, now, sm;
    int i, flag = 1;

    if ( n == 0 ) return -1./0.; /* -inf */
    if ( n < 0 ) return 0./0.;   /* NaN*/
    if ( n < 1 ) n = 1.0 / n, flag = -1; /* for extremely small n, use e^-x = 1/n */

    /* the cutoff iteration is 650, as over e**650, term multiplication would
       overflow. For larger numbers, the loop dominates the arctanh approximation
       loop (with having 13-15 iterations on average for tested numbers so far */

    for ( term = 1; term < n && lgVal < 650 ; term *= ee, lgVal++ );
    if ( lgVal == 650 ){
        n /= term;
        for ( term = 1 ; term < n ; term *= ee, lgVal++ );
    }
    n /= term;

    /* log(x) = 2 arctanh((x-1)/(x+1)) */
    n = (1 - n)/(n + 1);

    now = term = n;
    n *= n;
    sm = 0;

    /* limiting the iteration for worst case scenario, maximum 24 iteration */
    for ( i = 3 ; i < 50 ; i += 2 ){
        sm += now;
        term *= n;
        now = term / i;
        if ( now < 1e-17 ) break;
    }

    lgVal -= 2*sm;

    if ( flag == -1 ) lgVal = -lgVal;

    return lgVal;
}

คำตอบ:


17

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

log2.15.1

f(x)doublen12

n1.7976e+308term=infn=11017nterm *= e709.78266108405500745

1030000

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

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

x~y~=f~(x~)y=f(x~)ที่ถูกต้อง?) สิ่งนี้ไม่เหมือนกับการแสดงให้เห็นว่าชุด Taylor มาบรรจบกันเนื่องจากข้อผิดพลาดของการปัดเศษทศนิยม

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

5.เนื่องจากคุณรู้ตั้งแต่เริ่มต้นความแม่นยำของคู่คุณไม่จำเป็นต้องวนซ้ำ: จำนวนการวนซ้ำสามารถคำนวณได้ล่วงหน้า (อาจประมาณ 50) ใช้สิ่งนี้เพื่อลบสาขาจากรหัสของคุณหรืออย่างน้อยตั้งค่าจำนวนการทำซ้ำล่วงหน้า

แนวคิดทั่วไปทั้งหมดเกี่ยวกับการคลายการวนซ้ำก็มีผลเช่นกัน

6.เป็นไปได้ที่จะใช้เทคนิคการประมาณค่าอื่นนอกเหนือจากซีรี่ส์อนุกรม นอกจากนี้ยังมีชุดเซฟ (ที่มีการกำเริบ Clenshaw) approximants Pade และบางครั้งรากหาวิธีการเช่นวิธีของนิวตันเมื่อใดก็ตามที่ฟังก์ชั่นของคุณสามารถแต่งเป็นรากของฟังก์ชั่นที่เรียบง่าย (เช่นเคล็ดลับ sqrt ที่มีชื่อเสียง )

เศษส่วนต่อเนื่องอาจจะไม่ใหญ่เกินไปเนื่องจากเกี่ยวข้องกับการแบ่งซึ่งมีราคาแพงกว่าการเพิ่ม / เพิ่ม ถ้าคุณดู_mm_div_ssที่ https://software.intel.com/sites/landingpage/IntrinsicsGuide/ส่วนมีความล่าช้า 13-14 รอบและผ่านของ 5-14 ขึ้นอยู่กับสถาปัตยกรรมเมื่อเทียบกับ 3-5 / 0.5-1 สำหรับทวีคูณ / เพิ่ม / madd ดังนั้นโดยทั่วไป (ไม่เสมอไป) มันสมเหตุสมผลที่จะพยายามกำจัดฝ่ายให้ได้มากที่สุด

น่าเสียดายคณิตศาสตร์ไม่ได้เป็นคู่มือที่ดีเช่นนี้เพราะการแสดงออกที่มีสูตรสั้น ๆนั้นไม่จำเป็นต้องเป็นคำที่เร็วที่สุด คณิตศาสตร์ไม่ได้ลงโทษฝ่ายต่างๆ

x=m×2em12<m1exfrexp

8.เปรียบเทียบของคุณlogกับlogในlibmหรือ openlibm(เช่น: https://github.com/JuliaLang/openlibm/blob/master/src/e_log.c ) นี่เป็นวิธีที่ง่ายที่สุดในการค้นหาว่าคนอื่นคิดอะไรแล้ว นอกจากนี้ยังมีรุ่นที่ได้รับการปรับแต่งเป็นlibm พิเศษสำหรับผู้ผลิต CPU โดยเฉพาะ แต่รุ่นเหล่านั้นมักจะไม่มีซอร์สโค้ดเผยแพร่

Boost :: sf มีฟังก์ชั่นพิเศษบางอย่าง แต่ไม่ใช่ฟังก์ชั่นพื้นฐาน อาจแนะนำให้ดูที่แหล่งที่มาของ log1p แม้ว่า: http://www.boost.org/doc/libs/1_58_0/libs/math/doc/html/math_toolkit/powers/log1p.html

นอกจากนี้ยังมีไลบรารีทางคณิตศาสตร์ที่มีความแม่นยำแบบโอเพ่นซอร์สเช่น mpfr ซึ่งอาจใช้อัลกอริทึมที่แตกต่างจาก libm เนื่องจากต้องการความแม่นยำสูงกว่า

9.ความแม่นยำและความเสถียรของอัลกอริธึมเชิงตัวเลขของ Higham เป็นการแนะนำระดับบนที่ดีในการวิเคราะห์ข้อผิดพลาดของอัลกอริธึมเชิงตัวเลข สำหรับอัลกอริทึมการประมาณตัวเองทฤษฎีการประมาณค่าประมาณโดย Trefethen เป็นข้อมูลอ้างอิงที่ดี

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


26414e15

1.13e13term

 1e8

1
k=11071lnk

2
frexp x=m×2elnx=eln2+lnm

5

คำตอบของคิริลล์ได้สัมผัสกับประเด็นที่เกี่ยวข้องจำนวนมากแล้ว ฉันต้องการที่จะขยายในบางส่วนของพวกเขาขึ้นอยู่กับประสบการณ์การออกแบบห้องสมุดคณิตศาสตร์ในทางปฏิบัติ ข้อควรทราบล่วงหน้า: นักออกแบบห้องสมุดคณิตศาสตร์มักจะใช้การเพิ่มประสิทธิภาพอัลกอริธึมที่เผยแพร่ทุกครั้งรวมถึงการปรับให้เหมาะสมเฉพาะกับเครื่องจำนวนมากซึ่งจะไม่เผยแพร่ทั้งหมด รหัสมักจะเขียนด้วยภาษาแอสเซมบลีแทนที่จะใช้รหัสที่คอมไพล์แล้ว ดังนั้นจึงไม่น่าเป็นไปได้ที่การดำเนินการที่ตรงไปตรงมาและรวบรวมจะบรรลุผลมากกว่า 75% ของประสิทธิภาพของการใช้งานห้องสมุดคณิตศาสตร์ที่มีคุณภาพสูงที่มีอยู่สมมติว่าชุดคุณสมบัติที่เหมือนกัน (ความถูกต้องการจัดการกรณีพิเศษ

explogerfcΓ

โดยทั่วไปจะมีการประเมินความแม่นยำโดยการเปรียบเทียบกับการอ้างอิงที่มีความแม่นยำสูงกว่า (บุคคลที่สาม) ฟังก์ชั่นความแม่นยำเดี่ยวแบบข้อโต้แย้งเดียวสามารถทดสอบได้อย่างละเอียดถี่ถ้วนและฟังก์ชั่นอื่น ๆ ต้องการการทดสอบด้วยเวกเตอร์แบบทดสอบ (ชี้นำ) เห็นได้ชัดว่าไม่มีใครสามารถคำนวณผลการอ้างอิงที่แม่นยำได้อย่างไร้ขีด จำกัด แต่การวิจัยในDilemma ของ Table-Makerแสดงให้เห็นว่าสำหรับฟังก์ชั่นที่เรียบง่ายมันเพียงพอที่จะคำนวณการอ้างอิงด้วยความแม่นยำประมาณสามเท่าของความแม่นยำเป้าหมาย ดูตัวอย่าง:

Vincent Lefèvre, Jean-Michel Muller, "กรณีที่แย่ที่สุดสำหรับการปัดเศษฟังก์ชันรอบต้นอย่างถูกต้องด้วยความแม่นยำสองเท่า" ในการประชุมวิชาการ IEEE ครั้งที่ 15 เรื่องการคำนวณทางคอมพิวเตอร์ , 2001,111-118) (พิมพ์ออนไลน์)

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

ล.โอก.(1+x)=พี(x)log(x)=2atanh((x1)/(x+1))=p(((x1)/(x+1))2)p

การดำเนินการเพิ่มทวีคูณผสม ( FMA ) ซึ่งเปิดตัวครั้งแรกโดย IBM เมื่อ 25 ปีที่แล้วและขณะนี้มีอยู่ในสถาปัตยกรรมโปรเซสเซอร์ที่สำคัญทั้งหมดซึ่งเป็นหน่วยการสร้างที่สำคัญของการใช้งานห้องสมุดคณิตศาสตร์ที่ทันสมัย มันมีการปัดเศษลดข้อผิดพลาดให้ความคุ้มครองที่ จำกัด กับการยกเลิกการลดและช่วยลดความยุ่งยากอย่างมากมายสองครั้งสองครั้งเลขคณิต

แบบอย่าง IEEE-754 ความแม่นยำสองC99การดำเนินการlog()ดังต่อไปนี้แสดงให้เห็นถึงการใช้งานของ FMA (ที่เปิดเผยในC99fma()233

#include <math.h>

/* compute natural logarithm

   USE_ATANH == 1: maximum error found: 0.83482 ulp @ 0.7012829191167614
   USE_ATANH == 0: maximum error found: 0.83839 ulp @ 1.2788954397331760
*/
double my_log (double a)
{
    const double LOG2_HI = 0x1.62e42fefa39efp-01; // 6.9314718055994529e-01
    const double LOG2_LO = 0x1.abc9e3b39803fp-56; // 2.3190468138462996e-17
    double m, r, i, s, t, p, f, q;
    int e;

    m = frexp (a, &e);
    if (m < 0.70703125) { // 181/256
        m = m + m;
        e = e - 1;
    }
    i = (double)e;

    /* m in [181/256, 362/256] */

#if USE_ATANH
    /* Compute q = (m-1) / (m+1) */
    p = m + 1.0;
    m = m - 1.0;
    q = m / p;

    /* Compute (2*atanh(q)/q-2*q) as p(q**2), q in [-75/437, 53/309] */
    s = q * q;
    r =             0x1.2f1da230fb057p-3;  // 1.4800574027992994e-1
    r = fma (r, s,  0x1.399f73f934c01p-3); // 1.5313616375223663e-1
    r = fma (r, s,  0x1.7466542530accp-3); // 1.8183580149169243e-1
    r = fma (r, s,  0x1.c71c51a8bf129p-3); // 2.2222198291991305e-1
    r = fma (r, s,  0x1.249249425f140p-2); // 2.8571428744887228e-1
    r = fma (r, s,  0x1.999999997f6abp-2); // 3.9999999999404662e-1
    r = fma (r, s,  0x1.5555555555593p-1); // 6.6666666666667351e-1
    r = r * s;

    /* log(a) = 2*atanh(q) + i*log(2) = LOG2_LO*i + p(q**2)*q + 2q + LOG2_HI*i.
       Use K.C. Ng's trick to improve the accuracy of the computation, like so:
       p(q**2)*q + 2q = p(q**2)*q + q*t - t + m, where t = m**2/2.
    */
    t = m * m * 0.5;
    r = fma (q, t, fma (q, r, LOG2_LO * i)) - t + m;
    r = fma (LOG2_HI, i, r);

#else // USE_ATANH

    /* Compute f = m -1 */
    f = m - 1.0;
    s = f * f;

    /* Approximate log1p (f), f in [-75/256, 106/256] */
    r = fma (-0x1.961d64ddd82b6p-6, f, 0x1.d35fd598b1362p-5); // -2.4787281515616676e-2, 5.7052533321928292e-2
    t = fma (-0x1.fcf5138885121p-5, f, 0x1.b97114751d726p-5); // -6.2128580237329929e-2, 5.3886928516403906e-2
    r = fma (r, s, t);
    r = fma (r, f, -0x1.b5b505410388dp-5); // -5.3431043874398211e-2
    r = fma (r, f,  0x1.dd660c0bd22dap-5); //  5.8276198890387668e-2
    r = fma (r, f, -0x1.00bda5ecdad6fp-4); // -6.2680862565391612e-2
    r = fma (r, f,  0x1.1159b2e3bd0dap-4); //  6.6735934054864471e-2
    r = fma (r, f, -0x1.2489f14dd8883p-4); // -7.1420614809115476e-2
    r = fma (r, f,  0x1.3b0ee248a0ccfp-4); //  7.6918491287915489e-2
    r = fma (r, f, -0x1.55557d3b497c3p-4); // -8.3333481965921982e-2
    r = fma (r, f,  0x1.745d4666f7f48p-4); //  9.0909266480136641e-2
    r = fma (r, f, -0x1.999999d959743p-4); // -1.0000000092767629e-1
    r = fma (r, f,  0x1.c71c70bbce7c2p-4); //  1.1111110722131826e-1
    r = fma (r, f, -0x1.fffffffa61619p-4); // -1.2499999991822398e-1
    r = fma (r, f,  0x1.249249262c6cdp-3); //  1.4285714290377030e-1
    r = fma (r, f, -0x1.555555555f03cp-3); // -1.6666666666776730e-1
    r = fma (r, f,  0x1.999999999759ep-3); //  1.9999999999974433e-1
    r = fma (r, f, -0x1.fffffffffff53p-3); // -2.4999999999999520e-1
    r = fma (r, f,  0x1.555555555555dp-2); //  3.3333333333333376e-1
    r = fma (r, f, -0x1.0000000000000p-1); // -5.0000000000000000e-1

    /* log(a) = log1p (f) + i * log(2) */
    p = fma ( LOG2_HI, i, f);
    t = fma (-LOG2_HI, i, p);
    f = fma ( LOG2_LO, i, f - t);
    r = fma (r, s, f);
    r = r + p;
#endif // USE_ATANH

    /* Handle special cases */
    if (!((a > 0.0) && (a <= 0x1.fffffffffffffp1023))) {
        r = a + a;  // handle inputs of NaN, +Inf
        if (a  < 0.0) r =  0.0 / 0.0; //  NaN
        if (a == 0.0) r = -1.0 / 0.0; // -Inf
    }
    return r;
}

(+1) คุณรู้หรือไม่ว่าการใช้งานโอเพ่นซอร์สทั่วไป (เช่น openlibm) นั้นดีเท่าที่จะเป็นไปได้หรือไม่สามารถปรับปรุงฟังก์ชั่นพิเศษของพวกเขาได้หรือไม่?
คิริลล์

1
@Kirill ครั้งล่าสุดที่ฉันดูการใช้งานโอเพ่นซอร์ส (หลายปีที่ผ่านมา) พวกเขาไม่ได้ใช้ประโยชน์จาก FMA ในเวลาที่ IBM Power และ Intel Itanium เป็นสถาปัตยกรรมเดียวที่รวมการดำเนินการตอนนี้การสนับสนุนฮาร์ดแวร์สำหรับมันแพร่หลาย ยิ่งไปกว่านั้นการประมาณด้วยตารางบวก - พหุนามเป็นสถานะของยุคสมัยปัจจุบันตอนนี้ตารางไม่เป็นที่นิยม: ผลการเข้าถึงหน่วยความจำในการใช้พลังงานที่สูงขึ้นพวกเขาสามารถ (และทำ) ยุ่งเกี่ยวกับ vectorization ได้ ส่งผลให้ประสิทธิภาพเชิงลบที่อาจเกิดขึ้นจากตาราง
njuffa
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.