คอมพิวเตอร์: คุณทำคณิตศาสตร์


13

ความท้าทายนี้เป็นส่วนหนึ่งของความท้าทายของอัลกอริทึมซึ่งเกี่ยวข้องกับคณิตศาสตร์และเป็นความท้าทายของโค้ดที่เร็วที่สุด

สำหรับบางจำนวนเต็มบวกnพิจารณาสตริงแบบสุ่มอย่างสม่ำเสมอ1และ0s ของความยาวและเรียกมันว่าn Aตอนนี้ยังพิจารณาสตริงแบบสุ่มที่สองได้รับการแต่งตั้งอย่างสม่ำเสมอของความยาวnที่มีค่าเป็น-1, 0,หรือและเรียกมันว่า1 B_preตอนนี้ขอBเป็น+B_pre B_preนั่นคือการB_preตัดแบ่งตัวเอง

ตอนนี้พิจารณาผลิตภัณฑ์ภายในAและB[j,...,j+n-1]และเรียกว่าและดัชนีจากZ_j1

งาน

ผลลัพธ์ควรเป็นรายการของn+1เศษส่วน iระยะ TH ในการส่งออกที่ควรจะเป็นที่แน่นอนน่าจะเป็นที่ทุกแรกiแง่Z_jที่มีค่าเท่ากันj <= i0

คะแนน

ที่ใหญ่ที่สุดnที่รหัสของคุณจะช่วยให้การส่งออกที่ถูกต้องในอายุต่ำกว่า 10 นาทีในเครื่องของฉัน

Tie Breaker

หากคำตอบสองข้อมีคะแนนเท่ากันคำตอบที่ชนะจะได้รับก่อน

ในกรณีที่ไม่น่าเป็นไปได้มาก (มาก) ที่ใครบางคนพบวิธีที่จะได้รับคะแนนไม่ จำกัด หลักฐานที่ถูกต้องแรกของการแก้ปัญหาดังกล่าวจะได้รับการยอมรับ

เปรย

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

ภาษาและห้องสมุด

คุณสามารถใช้ภาษาใดก็ได้ที่มีคอมไพเลอร์ / ล่าม / อื่น ๆ สำหรับ Linux และไลบรารี่ใด ๆ ที่มีให้ใช้งานบน Linux ได้อย่างอิสระ

เครื่องของฉัน การจับเวลาจะทำงานบนเครื่องของฉัน นี่คือการติดตั้ง Ubuntu มาตรฐานบนโปรเซสเซอร์ AMD FX-8350 Eight-Core นี่ก็หมายความว่าฉันต้องสามารถเรียกใช้รหัสของคุณได้ ดังนั้นโปรดใช้ซอฟต์แวร์ฟรีที่มีให้ง่ายและโปรดรวมคำแนะนำทั้งหมดในการรวบรวมและเรียกใช้รหัสของคุณ


ผลการทดสอบบางอย่าง พิจารณาเฉพาะเอาต์พุตแรกสำหรับแต่ละnรายการ i=1นั่นคือเมื่อ สำหรับn1 ถึง 13 พวกเขาควรจะเป็น

 1: 4/6
 2: 18/36
 3: 88/216
 4: 454/1296
 5: 2424/7776
 6: 13236/46656
 7: 73392/279936
 8: 411462/1679616
 9: 2325976/10077696
10: 13233628/60466176
11: 75682512/362797056
12: 434662684/2176782336
13: 2505229744/13060694016

นอกจากนี้คุณยังสามารถหาสูตรทั่วไปi=1ที่http://oeis.org/A081671

กระดานผู้นำ (แยกตามภาษา)

  • n = 15. Python + Python ขนาน + pypyใน 1 นาที 49s โดย Jakube
  • n = 17. C ++ใน 3 นาที 37 วินาทีโดย Keith Randall
  • n = 16. C ++ใน 2 นาที 38 วินาทีโดย kuroi neko

1
@ Knerd ฉันจะบอกว่าไม่ ฉันจะพยายามหาวิธีเรียกใช้โค้ดของคุณใน linux แต่ก็ยังช่วยได้มาก

ตกลงขออภัยที่ลบความคิดเห็น เพราะทุกคนที่ไม่ได้อ่านก็คือถ้า F # หรือ C # จะได้รับอนุญาต :)
Knerd

คำถามอื่นอีกครั้งคุณอาจมีตัวอย่างของผลลัพธ์ที่ถูกต้องหรือไม่
Knerd

กราฟิกการ์ดของคุณคืออะไร? ดูเหมือนว่างานสำหรับ GPU
Michael M.

1
@Knerd ฉันเพิ่มตารางความน่าจะเป็นของคำถามแทน ฉันหวังว่ามันจะเป็นประโยชน์

คำตอบ:


5

C ++, n = 18 ใน 9 นาทีสำหรับ 8 เธรด

(แจ้งให้เราทราบหากเครื่องของคุณใช้เวลาน้อยกว่า 10 นาที)

ฉันใช้ประโยชน์จากสมมาตรหลายรูปแบบในอาร์เรย์ B สิ่งเหล่านี้เป็นวงจร (เลื่อนไปหนึ่งตำแหน่ง) กลับด้าน (กลับลำดับขององค์ประกอบ) และลงชื่อ (รับลบของแต่ละองค์ประกอบ) ก่อนอื่นฉันคำนวณรายการ Bs ที่เราต้องลองและน้ำหนัก จากนั้น B แต่ละตัวจะถูกเรียกใช้ผ่านรูทีนที่รวดเร็ว (โดยใช้คำสั่ง bitcount) สำหรับค่า 2 ^ n ทั้งหมดของ A

นี่คือผลลัพธ์สำหรับ n == 18:

> time ./a.out 18
 1: 16547996212044 / 101559956668416
 2:  3120508430672 / 101559956668416
 3:   620923097438 / 101559956668416
 4:   129930911672 / 101559956668416
 5:    28197139994 / 101559956668416
 6:     6609438092 / 101559956668416
 7:     1873841888 / 101559956668416
 8:      813806426 / 101559956668416
 9:      569051084 / 101559956668416
10:      510821156 / 101559956668416
11:      496652384 / 101559956668416
12:      493092812 / 101559956668416
13:      492186008 / 101559956668416
14:      491947940 / 101559956668416
15:      491889008 / 101559956668416
16:      449710584 / 101559956668416
17:      418254922 / 101559956668416
18:      409373626 / 101559956668416

real    8m55.854s
user    67m58.336s
sys 0m5.607s

รวบรวมโปรแกรมด้านล่างด้วย g++ --std=c++11 -O3 -mpopcnt dot.cc

#include <stdio.h>
#include <stdlib.h>
#include <vector>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

typedef long long word;

word n;

void inner(word bpos, word bneg, word w, word *cnt) {
    word maxi = n-1;
    for(word a = (1<<n)-1; a >= 0; a--) {
        word m = a;
        for(word i = maxi; i >= 0; i--, m <<= 1) {
            if(__builtin_popcount(m&bpos) != __builtin_popcount(m&bneg))
                break;
            cnt[i]+=w;
        }
    }
}

word pow(word n, word e) {
    word r = 1;
    for(word i = 0; i < e; i++) r *= n;
    return r;
}

typedef struct {
    word b;
    word weight;
} Bentry;

mutex block;
Bentry *bqueue;
word bhead;
word btail;
word done = -1;

word maxb;

// compute -1*b
word bneg(word b) {
    word w = 1;
    for(word i = 0; i < n; i++, w *= 3) {
        word d = b / w % 3;
        if(d == 1)
            b += w;
        if(d == 2)
            b -= w;
    }
    return b;
}

// rotate b one position
word brot(word b) {
    b *= 3;
    b += b / maxb;
    b %= maxb;
    return b;
}

// reverse b
word brev(word b) {
    word r = 0;
    for(word i = 0; i < n; i++) {
        r *= 3;
        r += b % 3;
        b /= 3;
    }
    return r;
}

// individual thread's work routine
void work(word *cnt) {
    while(true) {
        // get a queue entry to work on
        block.lock();
        if(btail == done) {
            block.unlock();
            return;
        }
        if(bhead == btail) {
            block.unlock();
            this_thread::sleep_for(chrono::microseconds(10));
            continue;
        }
        word i = btail++;
        block.unlock();

        // thread now owns bqueue[i], work on it
        word b = bqueue[i].b;
        word w = 1;
        word bpos = 0;
        word bneg = 0;
        for(word j = 0; j < n; j++, b /= 3) {
            word d = b % 3;
            if(d == 1)
                bpos |= 1 << j;
            if(d == 2)
                bneg |= 1 << j;
        }
        bpos |= bpos << n;
        bneg |= bneg << n;
        inner(bpos, bneg, bqueue[i].weight, cnt);
    }
}

int main(int argc, char *argv[]) {
    n = atoi(argv[1]);

    // allocate work queue
    maxb = pow(3, n);
    bqueue = (Bentry*)(malloc(maxb*sizeof(Bentry)));

    // start worker threads
    word procs = thread::hardware_concurrency();
    vector<thread> threads;
    vector<word*> counts;
    for(word p = 0; p < procs; p++) {
        word *cnt = (word*)calloc(64+n*sizeof(word), 1);
        threads.push_back(thread(work, cnt));
        counts.push_back(cnt);
    }

    // figure out which Bs we actually want to test, and with which weights
    bool *bmark = (bool*)calloc(maxb, 1);
    for(word i = 0; i < maxb; i++) {
        if(bmark[i]) continue;
        word b = i;
        word w = 0;
        for(word j = 0; j < 2; j++) {
            for(word k = 0; k < 2; k++) {
                for(word l = 0; l < n; l++) {
                    if(!bmark[b]) {
                        bmark[b] = true;
                        w++;
                    }
                    b = brot(b);
                }
                b = bneg(b);
            }
            b = brev(b);
        }
        bqueue[bhead].b = i;
        bqueue[bhead].weight = w;
        block.lock();
        bhead++;
        block.unlock();
    }
    block.lock();
    done = bhead;
    block.unlock();

    // add up results from threads
    word *cnt = (word*)calloc(n,sizeof(word));
    for(word p = 0; p < procs; p++) {
        threads[p].join();
        for(int i = 0; i < n; i++) cnt[i] += counts[p][i];
    }
    for(word i = 0; i < n; i++)
        printf("%2lld: %14lld / %14lld\n", i+1, cnt[n-1-i], maxb<<n);
    return 0;
}

ดีนั่นทำให้ฉันทำงานต่อไปกับสัตว์เลี้ยงสัตว์เลี้ยงของตัวเอง ...

ขอบคุณสำหรับสิ่งนี้. คุณมีรายการที่ชนะในปัจจุบัน เราต้องจำ-pthreadอีกครั้ง ฉันไป n=17ที่เครื่องของฉัน

โอ๊ะโอ .. คุณควรได้รับเงินเต็มจำนวน ขอโทษฉันพลาดกำหนด

@ Lembik: ไม่มีปัญหา
Keith Randall

6

Python 2 ใช้ pypy และ pp: n = 15 ใน 3 นาที

นอกจากนี้ยังเป็นเพียงกำลังดุร้ายที่เรียบง่าย น่าสนใจที่จะเห็นว่าฉันเกือบจะได้ความเร็วเดียวกันกับ kuroi neko กับ C ++ รหัสของฉันสามารถเข้าถึงได้n = 12ในเวลาประมาณ 5 นาที และฉันจะรันมันบนคอร์เสมือนหนึ่งแกนเท่านั้น

แก้ไข: ลดพื้นที่การค้นหาด้วยปัจจัย n

ผมสังเกตเห็นว่าเวกเตอร์กรณืA*การAผลิตตัวเลขเดียวกับความน่าจะเป็น (หมายเลขเดียวกัน) เป็นเวกเตอร์เดิมเมื่อผมย้ำไปA Bเช่นเวกเตอร์(1, 1, 0, 1, 0, 0)มีความน่าจะเป็นเช่นเดียวกับแต่ละเวกเตอร์(1, 0, 1, 0, 0, 1), (0, 1, 0, 0, 1, 1), (1, 0, 0, 1, 1, 0), (0, 0, 1, 1, 0, 1)และเมื่อเลือกแบบสุ่ม(0, 1, 1, 0, 1, 0) Bดังนั้นผมจึงไม่ได้มีการย้ำไปแต่ละเหล่านี้ 6 เวกเตอร์ แต่เพียงประมาณ 1 และแทนที่ด้วยcount[i] += 1count[i] += cycle_number

ซึ่งจะช่วยลดความซับซ้อนจากการTheta(n) = 6^n Theta(n) = 6^n / nดังนั้นn = 13มันเร็วกว่ารุ่นก่อนหน้าประมาณ 13 เท่า มันจะคำนวณn = 13ในประมาณ 2 นาที 20 วินาที เพราะn = 14มันยังช้าไปหน่อย ใช้เวลาประมาณ 13 นาที

แก้ไข 2: Multi-core-programming

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

รหัสที่สามารถใช้ได้ใน repo GitHub นี้: การเชื่อมโยง การเขียนโปรแกรมแบบมัลติคอร์ทำให้น่าเกลียดเล็กน้อย

แก้ไข 3: การลดพื้นที่การค้นหาสำหรับAเวกเตอร์และBเวกเตอร์

ฉันสังเกตเห็นความสมมาตรของกระจกแบบเดียวกันกับเวกเตอร์ที่Aเหมือนกันกับคุโรอิเนโกะ ยังไม่แน่ใจว่าทำไมจึงใช้งานได้ (และหากใช้งานได้กับแต่ละรายการn)

การลดพื้นที่การค้นหาสำหรับBเวกเตอร์เป็นเรื่องที่ฉลาดขึ้นเล็กน้อย ฉันแทนที่รุ่นเวกเตอร์ ( itertools.product) ด้วยฟังก์ชันของตัวเอง โดยทั่วไปฉันเริ่มต้นด้วยรายการที่ว่างเปล่าและวางมันลงบนสแต็ก จนกว่าสแต็กจะว่างเปล่าฉันจะลบรายการถ้ามันไม่ได้มีความยาวเท่าnกันฉันจะสร้าง 3 รายการอื่น ๆ (โดยการต่อท้าย -1, 0, 1) และผลักมันลงบนสแต็ก ฉันมีรายการที่มีความยาวเท่าnกันฉันสามารถประเมินผลรวม

ตอนนี้ฉันสร้างเวกเตอร์ด้วยตัวเองฉันสามารถกรองมันขึ้นอยู่กับว่าฉันสามารถเข้าถึงผลรวม = 0 หรือไม่ เช่นถ้าเวกเตอร์ของฉันAเป็น(1, 1, 1, 0, 0)และเวกเตอร์ของฉันBรูปลักษณ์ที่(1, 1, ?, ?, ?)ฉันรู้ว่าฉันไม่สามารถเติมเต็มด้วยค่าเพื่อให้? A*B = 0ดังนั้นฉันไม่ต้องทำซ้ำเวกเตอร์ทั้ง 6 Bตัว(1, 1, ?, ?, ?)นั้น

เราสามารถปรับปรุงเกี่ยวกับเรื่องนี้ถ้าเราไม่สนใจค่าสำหรับ 1. ตามที่ระบุไว้ในคำถามสำหรับค่าสำหรับi = 1เป็นลำดับA081671 มีหลายวิธีในการคำนวณเหล่านั้น a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / nฉันเลือกที่กำเริบง่าย: เนื่องจากเราสามารถคำนวณในพื้นไม่มีเวลาที่เราสามารถกรองพาหะมากขึ้นสำหรับi = 1 Bเช่นและA = (0, 1, 0, 1, 1) B = (1, -1, ?, ?, ?)เราสามารถละเว้นเวกเตอร์ได้โดยที่แรก? = 1เพราะA * cycled(B) > 0สำหรับเวกเตอร์ทั้งหมดเหล่านี้ ฉันหวังว่าคุณสามารถติดตาม มันอาจไม่ใช่ตัวอย่างที่ดีที่สุด

ด้วยวิธีนี้ฉันสามารถคำนวณn = 15ใน 6 นาที

แก้ไข 4:

นำความคิดที่ยอดเยี่ยมของ kuroi neko ไปใช้อย่างรวดเร็วซึ่งกล่าวว่าสิ่งนั้นBและ-Bให้ผลลัพธ์ที่เหมือนกัน เร่งความเร็ว x2 การติดตั้งใช้งานนั้นเป็นเพียงการแฮ็คข้อมูลอย่างรวดเร็วเท่านั้น n = 15ใน 3 นาที

รหัส:

สำหรับการเดินทางเยือนรหัสสมบูรณ์Github รหัสต่อไปนี้เป็นเพียงการแสดงคุณสมบัติหลัก ฉันออกจากการนำเข้าการเขียนโปรแกรมแบบมัลติคอร์พิมพ์ผล ...

count = [0] * n
count[0] = oeis_A081671(n)

#generating all important vector A
visited = set(); todo = dict()
for A in product((0, 1), repeat=n):
    if A not in visited:
        # generate all vectors, which have the same probability
        # mirrored and cycled vectors
        same_probability_set = set()
        for i in range(n):
            tmp = [A[(i+j) % n] for j in range(n)]
            same_probability_set.add(tuple(tmp))
            same_probability_set.add(tuple(tmp[::-1]))
        visited.update(same_probability_set)
        todo[A] = len(same_probability_set)

# for each vector A, create all possible vectors B
stack = []
for A, cycled_count in dict_A.iteritems():
    ones = [sum(A[i:]) for i in range(n)] + [0]
    # + [0], so that later ones[n] doesn't throw a exception
    stack.append(([0] * n, 0, 0, 0, False))

    while stack:
        B, index, sum1, sum2, used_negative = stack.pop()
        if index < n:
            # fill vector B[index] in all possible ways,
            # so that it's still possible to reach 0.
            if used_negative:
                for v in (-1, 0, 1):
                    sum1_new = sum1 + v * A[index]
                    sum2_new = sum2 + v * A[index - 1 if index else n - 1]
                    if abs(sum1_new) <= ones[index+1]:
                        if abs(sum2_new) <= ones[index] - A[n-1]:
                            C = B[:]
                            C[index] = v
                            stack.append((C, index + 1, sum1_new, sum2_new, True))
            else:
                for v in (0, 1):
                    sum1_new = sum1 + v * A[index]
                    sum2_new = sum2 + v * A[index - 1 if index else n - 1]
                    if abs(sum1_new) <= ones[index+1]:
                        if abs(sum2_new) <= ones[index] - A[n-1]:
                            C = B[:]
                            C[index] = v
                            stack.append((C, index + 1, sum1_new, sum2_new, v == 1))
        else:
            # B is complete, calculate the sums
            count[1] += cycled_count  # we know that the sum = 0 for i = 1
            for i in range(2, n):
                sum_prod = 0
                for j in range(n-i):
                    sum_prod += A[j] * B[i+j]
                for j in range(i):
                    sum_prod += A[n-i+j] * B[j]
                if sum_prod:
                    break
                else:
                    if used_negative:
                        count[i] += 2*cycled_count
                    else:
                        count[i] += cycled_count

การใช้งาน:

คุณต้องติดตั้ง pypy (สำหรับ Python 2 !!!) โมดูลขนานหลามไม่ได้รังเพลิงหลาม 3. จากนั้นคุณต้องติดตั้งขนานโมดูลหลามpp-1.6.4.zip สารสกัดจากมันลงในโฟลเดอร์และโทรcdpypy setup.py install

จากนั้นคุณสามารถเรียกโปรแกรมของฉันด้วย

pypy you-do-the-math.py 15

มันจะกำหนดจำนวนของซีพียูโดยอัตโนมัติ อาจมีข้อความผิดพลาดหลังจากเสร็จสิ้นโปรแกรมเพียงแค่ละเว้นพวกเขา n = 16ควรเป็นไปได้ในเครื่องของคุณ

เอาท์พุท:

Calculation for n = 15 took 2:50 minutes

 1  83940771168 / 470184984576  17.85%
 2  17379109692 / 470184984576   3.70%
 3   3805906050 / 470184984576   0.81%
 4    887959110 / 470184984576   0.19%
 5    223260870 / 470184984576   0.05%
 6     67664580 / 470184984576   0.01%
 7     30019950 / 470184984576   0.01%
 8     20720730 / 470184984576   0.00%
 9     18352740 / 470184984576   0.00%
10     17730480 / 470184984576   0.00%
11     17566920 / 470184984576   0.00%
12     17521470 / 470184984576   0.00%
13     17510280 / 470184984576   0.00%
14     17507100 / 470184984576   0.00%
15     17506680 / 470184984576   0.00%

หมายเหตุและความคิด:

  • ฉันมีโปรเซสเซอร์ i7-4600m ที่มี 2 คอร์และ 4 เธรด ไม่เป็นไรถ้าฉันใช้ 2 หรือ 4 เธรด การใช้งาน cpu คือ 50% พร้อม 2 เธรดและ 100% พร้อม 4 เธรด แต่ยังคงใช้เวลาเท่ากัน ฉันไม่รู้ว่าทำไม ฉันตรวจสอบแล้วว่าแต่ละเธรดมีเพียงครึ่งหนึ่งของข้อมูลเมื่อมี 4 เธรดตรวจสอบผลลัพธ์ ...
  • ฉันใช้รายการจำนวนมาก Python ไม่ค่อยมีประสิทธิภาพในการจัดเก็บฉันต้องคัดลอกรายการจำนวนมาก ... ดังนั้นฉันคิดว่าจะใช้จำนวนเต็มแทน ฉันสามารถใช้บิต 00 (สำหรับ 0) และ 11 (สำหรับ 1) ในเวกเตอร์ A และบิต 10 (สำหรับ -1), 00 (สำหรับ 0) และ 01 (สำหรับ 1) ในเวกเตอร์ B สำหรับผลิตภัณฑ์ ของ A และ B ฉันจะต้องคำนวณA & Bและนับบล็อก 01 และ 10 เท่านั้น การปั่นจักรยานสามารถทำได้ด้วยการขยับเวกเตอร์และใช้มาสก์ ... ฉันใช้งานทั้งหมดนี้ไปแล้ว แต่มันกลับกลายเป็นว่าช้ากว่ารายการ ฉันเดาว่า pypy เพิ่มประสิทธิภาพการดำเนินรายการอย่างแท้จริง

ในพีซีของฉันการวิ่ง n = 12 ใช้เวลา 7:25 ขณะที่ขยะ C ++ ของฉันใช้เวลา 1:23 ซึ่งทำให้เร็วขึ้นประมาณ 5 เท่า ด้วยแกนประมวลผลที่แท้จริงเพียงสองคอร์ CPU ของฉันจะได้รับปัจจัย 2.5 เท่าเมื่อเทียบกับแอปพลิเคชันแบบเธรดเดียวดังนั้นคอร์ที่แท้จริง 8 คอร์ควรรันบางสิ่งเช่นเร็วขึ้น 3 เท่าและนั่นไม่นับรวมกับการเพิ่มความเร็ว อายุของฉัน i3-2100 ไม่ว่าจะเป็นการผ่านห่วง C ++ ทั้งหมดเหล่านี้เพื่อรับมือกับเวลาในการคำนวณที่เพิ่มขึ้นอย่างทวีคูณนั้นคุ้มค่ากับความพยายามหรือไม่

ฉันได้รับความรู้สึกของcodegolf.stackexchange.com/questions/41021/… ... ลำดับ de Bruijn จะมีประโยชน์หรือไม่
kennytm

เกี่ยวกับมัลติเธรดคุณสามารถบีบคอร์ 2 + 2 ของคุณได้มากขึ้นโดยการล็อคแต่ละเธรด อัตราขยาย x2 เกิดจากตัวกำหนดตารางเวลาเลื่อนไปรอบ ๆ เธรดของคุณในแต่ละครั้งที่ย้ายก้านไม้ขีดในระบบ ด้วยการล็อคแกนคุณอาจได้รับ x2.5 แทน ไม่มีความคิดว่า Python อนุญาตให้ตั้งค่าความสัมพันธ์ของตัวประมวลผล

ขอบคุณฉันจะดูมัน แต่ฉันเป็นมือใหม่ในการทำงานแบบมัลติเธรด
Jakube

nbviewer.ipython.org/gist/minrk/5500077มีการเอ่ยถึงบางอย่างถึงแม้ว่าจะใช้เครื่องมือที่แตกต่างกันสำหรับการขนาน

5

พาลขน - C ++ - วิธีช้าเกินไป

เนื่องจากโปรแกรมเมอร์ที่ดีขึ้นได้นำ C ++ ไปใช้ฉันจึงโทรหาโปรแกรมนี้

#include <cstdlib>
#include <cmath>
#include <vector>
#include <bitset>
#include <future>
#include <iostream>
#include <iomanip>

using namespace std;

/*
6^^n events will be generated, so the absolute max
that can be counted by a b bits integer is
E(b*log(2)/log(6)), i.e. n=24 for a 64 bits counter

To enumerate 3 possible values of a size n vector we need
E(n*log(3)/log(2))+1 bits, i.e. 39 bits
*/
typedef unsigned long long Counter; // counts up to 6^^24

typedef unsigned long long Benumerator; // 39 bits
typedef unsigned long      Aenumerator; // 24 bits

#define log2_over_log6 0.3869

#define A_LENGTH ((size_t)(8*sizeof(Counter)*log2_over_log6))
#define B_LENGTH (2*A_LENGTH)

typedef bitset<B_LENGTH> vectorB;

typedef vector<Counter> OccurenceCounters;

// -----------------------------------------------------------------
// multithreading junk for CPUs detection and allocation
// -----------------------------------------------------------------
int number_of_CPUs(void)
{
    int res = thread::hardware_concurrency();
    return res == 0 ? 8 : res;
}

#ifdef __linux__
#include <sched.h>
void lock_on_CPU(int cpu)
{
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(cpu, &mask);
    sched_setaffinity(0, sizeof(mask), &mask);
}
#elif defined (_WIN32)
#include <Windows.h>
#define lock_on_CPU(cpu) SetThreadAffinityMask(GetCurrentThread(), 1 << cpu)
#else
// #warning is not really standard, so this might still cause compiler errors on some platforms. Sorry about that.
#warning "Thread processor affinity settings not supported. Performances might be improved by providing a suitable alternative for your platform"
#define lock_on_CPU(cpu)
#endif

// -----------------------------------------------------------------
// B values generator
// -----------------------------------------------------------------
struct Bvalue {
    vectorB p1;
    vectorB m1;
};

struct Bgenerator {
    int n;                 // A length
    Aenumerator stop;      // computation limit
    Aenumerator zeroes;    // current zeroes pattern
    Aenumerator plusminus; // current +1/-1 pattern
    Aenumerator pm_limit;  // upper bound of +1/-1 pattern

    Bgenerator(int n, Aenumerator start=0, Aenumerator stop=0) : n(n), stop(stop)
    {
        // initialize generator so that first call to next() will generate first value
        zeroes    = start - 1;
        plusminus = -1;
        pm_limit  = 0;
    }

    // compute current B value
    Bvalue value(void)
    {
        Bvalue res;
        Aenumerator pm = plusminus;
        Aenumerator position = 1;
        int i_pm = 0;
        for (int i = 0; i != n; i++)
        {
            if (zeroes & position)
            {
                if (i_pm == 0)  res.p1 |= position; // first non-zero value fixed to +1
                else         
                {
                    if (pm & 1) res.m1 |= position; // next non-zero values
                    else        res.p1 |= position;
                    pm >>= 1;
                }
                i_pm++;
            }
            position <<= 1;
        }
        res.p1 |= (res.p1 << n); // concatenate 2 Bpre instances
        res.m1 |= (res.m1 << n);
        return res;
    }

    // next value
    bool next(void)
    {
        if (++plusminus == pm_limit)
        {
            if (++zeroes == stop) return false;
            plusminus = 0;
            pm_limit = (1 << vectorB(zeroes).count()) >> 1;
        }
        return true;
    }

    // calibration: produces ranges that will yield the approximate same number of B values
    vector<Aenumerator> calibrate(int segments)
    {
        // setup generator for the whole B range
        zeroes = 0;
        stop = 1 << n;
        plusminus = -1;
        pm_limit = 0;

        // divide range into (nearly) equal chunks
        Aenumerator chunk_size = ((Aenumerator)pow (3,n)-1) / 2 / segments;

        // generate bounds for zeroes values
        vector<Aenumerator> res(segments + 1);
        int bound = 0;
        res[bound] = 1;
        Aenumerator count = 0;
        while (next()) if (++count % chunk_size == 0) res[++bound] = zeroes;
        res[bound] = stop;
        return res;
    }
};

// -----------------------------------------------------------------
// equiprobable A values merging
// -----------------------------------------------------------------
static char A_weight[1 << A_LENGTH];
struct Agroup {
    vectorB value;
    int     count;
    Agroup(Aenumerator a = 0, int length = 0) : value(a), count(length) {}
};
static vector<Agroup> A_groups;

Aenumerator reverse(Aenumerator n) // this works on N-1 bits for a N bits word
{
    Aenumerator res = 0;
    if (n != 0) // must have at least one bit set for the rest to work
    {
        // construct left-padded reverse value
        for (int i = 0; i != 8 * sizeof(n)-1; i++)
        {
            res |= (n & 1);
            res <<= 1;
            n >>= 1;
        }

        // shift right to elimitate trailing zeroes
        while (!(res & 1)) res >>= 1;
    }
    return res;
}

void generate_A_groups(int n)
{
    static bitset<1 << A_LENGTH> lookup(0);
    Aenumerator limit_A = (Aenumerator)pow(2, n);
    Aenumerator overflow = 1 << n;
    for (char & w : A_weight) w = 0;

    // gather rotation cycles
    for (Aenumerator a = 0; a != limit_A; a++)
    {
        Aenumerator rotated = a;
        int cycle_length = 0;
        for (int i = 0; i != n; i++)
        {
            // check for new cycles
            if (!lookup[rotated])
            {
                cycle_length++;
                lookup[rotated] = 1;
            }

            // rotate current value
            rotated <<= 1;
            if (rotated & overflow) rotated |= 1;
            rotated &= (overflow - 1);
        }

        // store new cycle
        if (cycle_length > 0) A_weight[a] = cycle_length;
    }

    // merge symetric groups
    for (Aenumerator a = 0; a != limit_A; a++)
    {
        // skip already grouped values
        if (A_weight[a] == 0) continue;

        // regroup a symetric pair
        Aenumerator r = reverse(a);
        if (r != a)
        {
            A_weight[a] += A_weight[r];
            A_weight[r] = 0;
        }  
    }

    // generate groups
    for (Aenumerator a = 0; a != limit_A; a++)
    {
        if (A_weight[a] != 0) A_groups.push_back(Agroup(a, A_weight[a]));
    }
}

// -----------------------------------------------------------------
// worker thread
// -----------------------------------------------------------------
OccurenceCounters solve(int n, int index, Aenumerator Bstart, Aenumerator Bstop)
{
    OccurenceCounters consecutive_zero_Z(n, 0);  // counts occurences of the first i terms of Z being 0

    // lock on assigned CPU
    lock_on_CPU(index);

    // enumerate B vectors
    Bgenerator Bgen(n, Bstart, Bstop);
    while (Bgen.next())
    {
        // get next B value
        Bvalue B = Bgen.value();

        // enumerate A vector groups
        for (const auto & group : A_groups)
        {
            // count consecutive occurences of inner product equal to zero
            vectorB sliding_A(group.value);
            for (int i = 0; i != n; i++)
            {
                if ((sliding_A & B.p1).count() != (sliding_A & B.m1).count()) break;
                consecutive_zero_Z[i] += group.count;
                sliding_A <<= 1;
            }
        }
    }
    return consecutive_zero_Z;
}

// -----------------------------------------------------------------
// main
// -----------------------------------------------------------------
#define die(msg) { cout << msg << endl; exit (-1); }

int main(int argc, char * argv[])
{
    int n = argc == 2 ? atoi(argv[1]) : 16; // arbitray value for debugging
    if (n < 1 || n > 24) die("vectors of lenght between 1 and 24 is all I can (try to) compute, guv");

    auto begin = time(NULL);

    // one worker thread per CPU
    int num_workers = number_of_CPUs();

    // regroup equiprobable A values
    generate_A_groups(n);

    // compute B generation ranges for proper load balancing
    vector<Aenumerator> ranges = Bgenerator(n).calibrate(num_workers);

    // set workers to work
    vector<future<OccurenceCounters>> workers(num_workers);
    for (int i = 0; i != num_workers; i++)
    {
        workers[i] = async(
            launch::async, // without this parameter, C++ will decide whether execution shall be sequential or asynchronous (isn't C++ fun?).
            solve, n, i, ranges[i], ranges[i+1]); 
    }

    // collect results
    OccurenceCounters result(n + 1, 0);
    for (auto& worker : workers)
    {
        OccurenceCounters partial = worker.get();
        for (size_t i = 0; i != partial.size(); i++) result[i] += partial[i]*2; // each result counts for a symetric B pair
    }
    for (Counter & res : result) res += (Counter)1 << n; // add null B vector contribution
    result[n] = result[n - 1];                           // the last two probabilities are equal by construction

    auto duration = time(NULL) - begin;

    // output
    cout << "done in " << duration / 60 << ":" << setw(2) << setfill('0') << duration % 60 << setfill(' ')
        << " by " << num_workers << " worker thread" << ((num_workers > 1) ? "s" : "") << endl;
    Counter events = (Counter)pow(6, n);
    int width = (int)log10(events) + 2;
    cout.precision(5);
    for (int i = 0; i <= n; i++) cout << setw(2) << i << setw(width) << result[i] << " / " << events << " " << fixed << (float)result[i] / events << endl;

    return 0;
}

สร้างปฏิบัติการ

เป็นแหล่ง C ++ 11 แบบสแตนด์อโลนที่รวบรวมโดยไม่มีคำเตือนและทำงานอย่างราบรื่นบน:

  • Win7 & MSVC2013
  • Win7 & MinGW - g ++ 4.7
  • Ubuntu & g ++ 4.8 (ใน VirtualBox VM พร้อม CPU 2 ตัวที่ถูกจัดสรร)

หากคุณคอมไพล์ด้วย g ++ ให้ใช้: g ++ -O3 -pthread -std = c ++ 11 การ
ลืม-pthreadจะทำให้การถ่ายโอนข้อมูลหลักที่ดีและเป็นมิตร

การเพิ่มประสิทธิภาพ

  1. เทอมสุดท้าย Z เท่ากับเทอมแรก (Bpre x A ในทั้งสองกรณี) ดังนั้นผลลัพธ์สองอันสุดท้ายจะเท่ากันเสมอซึ่งจ่ายการคำนวณค่า Z สุดท้าย
    กำไรนั้นถูกละเลย แต่การเขียนโค้ดไม่มีค่าใช้จ่ายใด ๆ ดังนั้นคุณอาจใช้มันเช่นกัน

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

  3. ดูเหมือนว่าคู่ของรูปแบบทางสมมาตรยังสร้างความน่าจะเป็นที่เหมือนกัน ยกตัวอย่างเช่น 100101 และ 101001
    ฉันไม่มีข้อพิสูจน์ทางคณิตศาสตร์ แต่อย่างสังหรณ์ใจเมื่อนำเสนอด้วยรูปแบบ B ที่เป็นไปได้ทั้งหมดค่า A symetric A แต่ละค่าจะมีความซับซ้อนกับค่า B ที่สอดคล้องกันสำหรับผลลัพธ์ระดับโลกเดียวกัน
    วิธีนี้ช่วยให้สามารถจัดกลุ่มเวกเตอร์ A ได้อีกเพื่อลดจำนวนกลุ่ม A ลงได้ประมาณ 30%

  4. ผิด ด้วยเหตุผลกึ่งลึกลับบางรูปแบบทั้งหมดที่มีเพียงหนึ่งหรือสองบิตตั้งค่าให้ผลลัพธ์เดียวกัน สิ่งนี้ไม่ได้แสดงถึงกลุ่มที่แตกต่างกันมากมาย แต่ก็ยังสามารถรวมกันได้โดยไม่มีค่าใช้จ่าย

  5. เวกเตอร์ B และ -B (B ที่มีส่วนประกอบทั้งหมดคูณด้วย -1) ให้ความน่าจะเป็นแบบเดียวกัน
    (เช่น [1, 0, -1, 1] และ [-1, 0, 1, -1])
    ยกเว้นเวกเตอร์ว่าง (ส่วนประกอบทั้งหมดเท่ากับ 0), B และ -B สร้างคู่เวกเตอร์ที่แตกต่างกัน
    สิ่งนี้ช่วยให้ลดจำนวนของค่า B ลงครึ่งหนึ่งโดยพิจารณาเพียงหนึ่งคู่ต่อหนึ่งและคูณผลงานด้วย 2 โดยเพิ่มการมีส่วนร่วมทั่วโลกที่รู้จักกันของค่า null B ต่อความน่าจะเป็นแต่ละครั้งเพียงครั้งเดียว

มันทำงานอย่างไร

จำนวนค่า B มีขนาดใหญ่มาก (3 n ) ดังนั้นการคำนวณล่วงหน้าจะต้องใช้หน่วยความจำที่ไม่เพียงพอซึ่งจะทำให้การคำนวณช้าลงและหมด RAM ในที่สุด
น่าเสียดายที่ฉันไม่สามารถหาวิธีง่ายๆในการแจกแจงค่า B ที่ปรับให้เหมาะสมครึ่งชุดได้ดังนั้นฉันจึงหันไปใช้การเขียนโปรแกรมสร้างเฉพาะ

เครื่องปั่นไฟ B อันยิ่งใหญ่สนุกไปกับการเขียนโค้ดถึงแม้ว่าภาษาที่สนับสนุนกลไกการให้ผลตอบแทนจะได้รับอนุญาตให้ตั้งโปรแกรมได้อย่างสง่างามกว่ามาก
สิ่งที่ทำสั้น ๆ คือพิจารณา "โครงกระดูก" ของเวกเตอร์ Bpre เป็นเวกเตอร์ไบนารีที่ 1s แทนค่าจริงหรือ -1
ในบรรดาค่าที่เป็นไปได้ +1 เหล่านี้ทั้งหมดค่าแรกจะถูกกำหนดเป็น +1 (ดังนั้นการเลือกหนึ่งในเวกเตอร์ B / -B ที่เป็นไปได้) และการรวมที่เป็นไปได้ + 1 / -1 ที่เหลือทั้งหมดจะถูกแจกแจง
สุดท้ายระบบสอบเทียบอย่างง่ายทำให้แน่ใจว่าเธรดผู้ปฏิบัติงานแต่ละคนจะประมวลผลช่วงของค่าประมาณขนาดเดียวกัน

ค่าจะถูกกรองอย่างมากสำหรับการจัดกลุ่มใหม่ในชิ้นส่วนที่สวมใส่ได้
สิ่งนี้ทำในช่วงการคำนวณล่วงหน้าที่ brute-force ตรวจสอบค่าที่เป็นไปได้ทั้งหมด
ส่วนนี้มีเวลาดำเนินการที่ถูกละเลย O (2 n ) และไม่จำเป็นต้องได้รับการปรับให้เหมาะสม (โค้ดที่อ่านแล้วไม่สามารถอ่านได้มากพอ!

ในการประเมินผลิตภัณฑ์ด้านใน (ซึ่งต้องทดสอบกับศูนย์เท่านั้น) ส่วนประกอบ -1 และ 1 ของ B จะถูกจัดกลุ่มใหม่เป็นเวกเตอร์ไบนารี
ผลิตภัณฑ์ชั้นในเป็นโมฆะถ้า (และเฉพาะในกรณีที่) มีจำนวนเท่ากับ +1 และ 1s ในหมู่ค่า B ที่สอดคล้องกับค่าที่ไม่ใช่ศูนย์
สิ่งนี้สามารถคำนวณได้ด้วยการดำเนินการปิดบังอย่างง่ายและการนับจำนวนบิตซึ่งช่วยในการstd::bitsetสร้างรหัสการนับบิตที่มีประสิทธิภาพพอสมควรโดยไม่ต้องใช้คำแนะนำที่น่าเกลียดอย่างแท้จริง

งานแบ่งออกเป็นสองส่วนเท่า ๆ กันระหว่างคอร์โดยมี CPU affinity บังคับ (ช่วยเล็กน้อยทุกอย่าง

ตัวอย่างผลลัพธ์

C:\Dev\PHP\_StackOverflow\C++\VectorCrunch>release\VectorCrunch.exe 16
done in 8:19 by 4 worker threads
 0  487610895942 / 2821109907456 0.17284
 1   97652126058 / 2821109907456 0.03461
 2   20659337010 / 2821109907456 0.00732
 3    4631534490 / 2821109907456 0.00164
 4    1099762394 / 2821109907456 0.00039
 5     302001914 / 2821109907456 0.00011
 6     115084858 / 2821109907456 0.00004
 7      70235786 / 2821109907456 0.00002
 8      59121706 / 2821109907456 0.00002
 9      56384426 / 2821109907456 0.00002
10      55686922 / 2821109907456 0.00002
11      55508202 / 2821109907456 0.00002
12      55461994 / 2821109907456 0.00002
13      55451146 / 2821109907456 0.00002
14      55449098 / 2821109907456 0.00002
15      55449002 / 2821109907456 0.00002
16      55449002 / 2821109907456 0.00002

การแสดง

มัลติเธรดควรทำงานอย่างสมบูรณ์แบบในเรื่องนี้แม้ว่าแกน "จริง" เท่านั้นที่จะมีส่วนช่วยในการคำนวณความเร็วได้อย่างเต็มที่ CPU ของฉันมีเพียง 2 คอร์สำหรับ 4 ซีพียูและการได้รับมากกว่ารุ่นเธรดเดียวคือ "เท่านั้น" ประมาณ 3.5

คอมไพเลอร์

ปัญหาเริ่มต้นของการใช้มัลติเธรดทำให้ฉันเชื่อว่าคอมไพเลอร์ GNU นั้นทำงานได้แย่กว่า Microsoft

หลังจากการทดสอบอย่างละเอียดยิ่งขึ้นดูเหมือนว่า g ++ จะชนะในวันนี้อีกครั้งโดยสร้างโค้ดเร็วขึ้นประมาณ 30% (อัตราส่วนเดียวกันกับที่ฉันสังเกตเห็นในโครงการคำนวณหนักอีกสองโครงการ)

โดยเฉพาะอย่างยิ่งstd::bitsetไลบรารีนั้นถูกใช้งานด้วยคำสั่งการนับบิตเฉพาะโดย g ++ 4.8 ในขณะที่ MSVC 2013 ใช้การเลื่อนบิตแบบทั่วไปเท่านั้น

ดังที่คาดไว้การรวบรวมใน 32 หรือ 64 บิตไม่สร้างความแตกต่าง

การปรับแต่งเพิ่มเติม

ฉันสังเกตเห็นกลุ่ม A จำนวนน้อยที่ผลิตความน่าจะเป็นแบบเดียวกันหลังจากการดำเนินการลดทั้งหมด แต่ฉันไม่สามารถระบุรูปแบบที่จะอนุญาตให้จัดกลุ่มใหม่ได้

นี่คือคู่ที่ฉันสังเกตเห็นสำหรับ n = 11:

  10001011 and 10001101
 100101011 and 100110101
 100101111 and 100111101
 100110111 and 100111011
 101001011 and 101001101
 101011011 and 101101011
 101100111 and 110100111
1010110111 and 1010111011
1011011111 and 1011111011
1011101111 and 1011110111

ฉันคิดว่าความน่าจะเป็นที่สองครั้งสุดท้ายควรเหมือนกันเสมอ เนื่องจากผลิตภัณฑ์ภายในที่ n + 1 นั้นเหมือนกับผลิตภัณฑ์แรก

สิ่งที่ฉันหมายถึงก็คือผลิตภัณฑ์ชั้นใน n ตัวแรกมีค่าเป็นศูนย์ถ้าหาก n + 1 ตัวแรกเท่านั้น ผลิตภัณฑ์ภายในที่ผ่านมาไม่ได้ให้ข้อมูลใหม่ใด ๆ เหมือนที่คุณเคยทำมาก่อน ดังนั้นจำนวนของสตริงที่ให้ผลิตภัณฑ์เป็นศูนย์จะเหมือนกับจำนวนที่ให้ผลิตภัณฑ์เป็นศูนย์เท่ากับ 1

จากความสนใจคุณคำนวณอะไรแทนกันแน่

ขอบคุณสำหรับการอัปเดต แต่ฉันไม่เข้าใจบรรทัด "0 2160009216 2176782336" คุณนับอะไรกันแน่ในกรณีนี้? ความน่าจะเป็นที่ผลิตภัณฑ์ภายในแรกมีค่าเป็นศูนย์นั้นเล็กกว่านั้นมาก

คุณสามารถให้คำแนะนำในการคอมไพล์และรันได้ไหม? ฉันพยายาม g ++ -Wall -std = c ++ 11 kuroineko.cpp -o kuroineko และ. / kuroineko 12 แต่มันให้terminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.