मैं एक साधारण प्रयोग करने की कोशिश कर रहा हूं जहां मैं यह जानना चाहता हूं कि जब आप सीपीयू गहन कार्यों का एक समूह प्राप्त करते हैं तो थ्रेड पूल का सही आकार क्या होता है।

मुझे पहले से ही पता है कि यह आकार मशीन पर कोर की संख्या के बराबर होना चाहिए, लेकिन मैं इसे अनुभवजन्य रूप से साबित करना चाहता हूं। यहाँ कोड है:

public class Main {

    public static void main(String[] args) throws ExecutionException {
        List<Future> futures = new ArrayList<>();
        ExecutorService threadPool = Executors.newFixedThreadPool(4);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 100; i++) {
            futures.add(threadPool.submit(new CpuBoundTask()));
        }

        for (int i = 0; i < futures.size(); i++) {
            futures.get(i).get();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Time = " + (endTime - startTime));
        threadPool.shutdown();
    }

    static class CpuBoundTask implements Runnable {
        @Override
        public void run() {
            int a = 0;
            for (int i = 0; i < 90000000; i++) {
                a = (int) (a + Math.tan(a));
            }
        }
    }
}

प्रत्येक कार्य लगभग 700 मिलीसेकंड में निष्पादित होता है (मुझे लगता है कि कम से कम एक बार ThreadScheduler द्वारा छूट दिए जाने के लिए पर्याप्त है)।

मैं इसे मैकबुकप्रो 2017, 3.1 गीगाहर्ट्ज़ इंटेल कोर i5, हाइपरथ्रेडिंग के साथ 2 भौतिक कोर सक्रिय, इसलिए 4 तार्किक सीपीयू पर चला रहा हूं।

मैंने थ्रेडपूल के आकार को समायोजित किया, और मैंने इस कार्यक्रम को कई बार (औसत समय) चलाया। यहाँ परिणाम हैं:

1 thread = 57 seconds
2 threads = 29 seconds
4 threads = 18 seconds
8 threads = 18.1 seconds
16 threads = 18.2 seconds
32 threads = 17.8 seconds
64 threads = 18.2 seconds

संदर्भ स्विच ओवरहेड की वजह से, मैं निष्पादन समय काफी अधिक होने की उम्मीद कर रहा था, एक बार जब मैं इतने सारे धागे (सीपीयू कोर की संख्या से अधिक) जोड़ता हूं, लेकिन ऐसा लगता है कि यह वास्तव में नहीं होता है।

मैंने प्रोग्राम की निगरानी के लिए विजुअलVM का उपयोग किया, और ऐसा लगता है कि सभी धागे बन गए हैं और वे अपेक्षित स्थिति में चल रहे हैं। साथ ही, ऐसा लगता है कि सीपीयू का ठीक से उपयोग किया जा रहा है (95% के करीब)।

क्या ऐसा कुछ है जो मुझे याद आ रहा है?

3
Cosmin Ioniță 19 पद 2020, 17:34

4 जवाब

सबसे बढ़िया उत्तर

इस मामले में आपको System.currentTimeMillis() के बजाय System.nanoTime() का उपयोग करना चाहिए। .

आपका एल्गोरिथ्म 4 थ्रेड्स पर स्केलिंग बंद कर देता है, सादगी के लिए, मान लें कि सभी थ्रेड्स ने समान संख्या में कार्य किए, इसलिए 25 प्रति थ्रेड। प्रत्येक थ्रेड को 25 पुनरावृत्तियों की गणना करने में 18 सेकंड कम या ज्यादा लगे।

बहुत ही सरल तरीके से, जब आप 64 थ्रेड्स के साथ दौड़ते हैं, तो आपके पास 8 थ्रेड्स प्रति कोर होंगे, और पहले 4 पुनरावृत्तियों के साथ 4 थ्रेड्स होंगे। (१ प्रति कोर) समानांतर में चल रहा है और अन्य 60 थ्रेड निष्क्रिय मोड में हैं, CPU संसाधनों की उनके पुनरावृत्तियों की गणना के लिए प्रतीक्षा कर रहे हैं, इसलिए आपके पास कुछ ऐसा है:

Iteration 0 : Thread 1 (running)
Iteration 1 : Thread 2 (running)
Iteration 2 : Thread 3 (running)
Iteration 3 : Thread 4 (running)
Iteration 4 : Thread 5 (waiting)
Iteration 5 : Thread 6 (waiting)
Iteration 6 : Thread 7 (waiting)
Iteration 7 : Thread 8 (waiting)
...
Iteration 63 : Thread 64 (waiting)

जब वे 4 धागे अपने पुनरावृत्तियों को पूरा करते हैं, तो वे प्रत्येक को एक और पुनरावृत्ति प्राप्त करेंगे। इस बीच, मान लें कि 5 से 8 तक के धागे अगले चार पुनरावृत्तियों पर काम करना शुरू कर देते हैं (फिर से 4 धागे समानांतर में काम करते हैं) जबकि अन्य धागे हैं सीपीयू की प्रतीक्षा में अवरुद्ध इत्यादि। तो आपके पास हमेशा 4 थ्रेड समानांतर में चल रहे हैं, भले ही, और इसीलिए:

8 threads = 18.1 seconds
16 threads = 18.2 seconds
32 threads = 17.8 seconds
64 threads = 18.2 seconds

आपके पास लगभग एक ही निष्पादन समय है, लगभग समान निष्पादन समय जो 4 थ्रेड्स को समानांतर में 25 पुनरावृत्तियों को पूरा करने में लगा।

क्योंकि यह बिना किसी समस्या के सीपीयू-बाउंड एल्गोरिथ्म है:

  1. तादात्म्य;
  2. असंतुलित लोड हो रहा है (यानी, प्रत्येक लूप पुनरावृत्ति में लगभग समान निष्पादन समय लगता है);
  3. मेमोरी बैंडविड्थ संतृप्ति;
  4. कैश अमान्य;
  5. झूठी साझेदारी।

जब आप थ्रेड्स की संख्या प्रति core बढ़ाते हैं, तो यह समग्र निष्पादन समय पर इतना प्रतिबिंबित नहीं करता है।

3
dreamcrash 19 पद 2020, 18:47

संदर्भ स्विच ओवरहेड की वजह से, मैं निष्पादन समय काफी अधिक होने की उम्मीद कर रहा था, एक बार जब मैं इतने सारे धागे (सीपीयू कोर की संख्या से अधिक) जोड़ता हूं, लेकिन ऐसा लगता है कि यह वास्तव में नहीं होता है।

कई कारणों से इसका पता लगाना बहुत कठिन होगा। सबसे पहले, आधुनिक ऑपरेटिंग सिस्टम इस उपयोग के मामले के अनुकूलन में बेहद अच्छे हैं। संदर्भ स्विचिंग एक बड़ा हथौड़ा हुआ करता था लेकिन आधुनिक मेमोरी आर्किटेक्चर के साथ, ऐसा करना बहुत कम खर्चीला है।

संदर्भ स्विचिंग के लिए दंड मेमोरी कैश फ्लशिंग है। जब एक थ्रेड को सीपीयू में स्वैप किया जाता है, तो स्थानीय कैश्ड मेमोरी में इसकी गणना करने के लिए आवश्यक प्रति-थ्रेड जानकारी नहीं हो सकती है। आवश्यक मेमोरी लाइनों को पढ़ने के लिए इसे मुख्य मेमोरी में जाना पड़ता है जो धीमी होती है। इसकी अदला-बदली करना भी धीमा है क्योंकि किसी भी गंदे पृष्ठ को मुख्य मेमोरी में लिखना होगा। इस कारण से, मुझे लगता है कि यदि आपका कार्य एक टन अधिक कैश्ड मेमोरी का उपयोग करता है, तो आपको एक उच्च संदर्भ स्विचिंग पेनल्टी दिखाई दे सकती है। आपका वर्तमान कार्यक्रम सिर्फ कुछ पूर्णांकों को संग्रहीत करता है। उदाहरण के लिए, मान लें कि आप प्रत्येक थ्रेड के लिए अपने प्रोग्राम की शुरुआत में ~10k आवंटित करते हैं और उसमें यादृच्छिक मान डालते हैं। फिर जब प्रत्येक थ्रेड चलता है, तो वे अपने संबंधित 10k खंड से डेटा को रैंडम एक्सेस करने का प्रयास करते हैं जो सीपीयू कैश्ड मेमोरी में चला जाएगा। यह एक बेहतर प्रयोग हो सकता है। लेकिन उसने कहा कि आपको अपने आर्किटेक्चर के बारे में बहुत कुछ जानना होगा और संदर्भ स्विच का पूरी तरह से पता लगाने के लिए अपने एप्लिकेशन को उचित रूप से अनुकूलित करना होगा।

अंत में, किसी भी जावा परीक्षण कार्यक्रम की तरह, आपको एक मिनट के लिए चलना चाहिए ताकि क्लास हॉट-स्वैपिंग और अन्य अनुकूलन व्यवस्थित हो जाएं, फिर लंबे समय तक डेटा एकत्र करना चलाएं। एक परीक्षण चलाना जिसमें 18 सेकंड लगते हैं, आपके परीक्षण कोड से अधिक JVM का प्रयोग कर रहा है। यदि आप 1800 सेकंड (मान लें) के लिए दौड़ते हैं तो आपको किसी प्रकार का मापन योग्य अंतर दिखाई दे सकता है। और, जैसा कि @dreamcrash ने उल्लेख किया है, System.nanoTime() का उपयोग इस तरह से बारीक समय की गणना के लिए किया जाना चाहिए।

2
Gray 21 पद 2020, 15:55

Executors.newWorkStealingPool

यदि आप Java 8 का उपयोग कर रहे हैं, तो workStealingThreadPool क्योंकि यह सर्वोत्तम परिणाम दे सकता है:

ExecutorService es = Executors.newWorkStealingPool();

सभी उपलब्ध प्रोसेसर अपने लक्ष्य समानांतरता स्तर के रूप में। समांतरता स्तर कार्य प्रसंस्करण में सक्रिय रूप से लगे या संलग्न होने के लिए उपलब्ध धागे की अधिकतम संख्या से मेल खाता है। थ्रेड्स की वास्तविक संख्या गतिशील रूप से बढ़ और घट सकती है। एक कार्य-चोरी पूल उस क्रम के बारे में कोई गारंटी नहीं देता है जिसमें सबमिट किए गए कार्यों को निष्पादित किया जाता है।

2
Basil Bourque 20 पद 2020, 02:11

सबसे पहले, यह धारणा कि संदर्भ स्विच ओवरहेड थ्रेड्स की संख्या के साथ बढ़ता है, हमेशा सही नहीं होता है। आपका नमूना कार्यक्रम निश्चित मात्रा में कार्य करता है। आपके पास जितने अधिक धागे होंगे - प्रत्येक थ्रेड जितना कम काम करेगा, और उतना ही कम CPU समय प्राप्त होगा।

यहां तक ​​​​कि जब आपके पास सैकड़ों धागे होते हैं, तो ओएस उनके बीच असीम रूप से अक्सर स्विच नहीं करेगा। आमतौर पर एक न्यूनतम अंतराल (समय का टुकड़ा) होता है जिसे बिना किसी छूट के चलने की अनुमति दी जाती है। भौतिक कोर के लिए प्रतिस्पर्धा करने वाले बहुत से थ्रेड्स के साथ, प्रत्येक थ्रेड को अपना सीपीयू टाइम स्लाइस कम बार (यानी भुखमरी) प्राप्त होगा, लेकिन संदर्भ स्विच की संख्या थ्रेड्स की संख्या के अनुपात में नहीं बढ़ेगी।

मैंने आपके प्रोग्राम में Linux perf के साथ संदर्भ स्विच की संख्या मापी:

perf stat -e context-switches java Main

और यहाँ परिणाम हैं:

 2 threads | 1,445 context-switches
 4 threads | 2,417 context-switches
 8 threads | 9,280 context-siwtches
16 threads | 9,257 context-switches
32 threads | 9,527 context-switches
64 threads | 9,986 context-switches

संदर्भ स्विच में एक बड़ी छलांग अपेक्षित रूप से तब होती है जब थ्रेड्स की संख्या भौतिक सीपीयू की संख्या से अधिक हो जाती है, लेकिन बाद में संख्या उतनी नहीं बढ़ती है।

ठीक है, हम लगभग 10K संदर्भ स्विच देखते हैं। इतना है? जैसा कि उत्तरों से पता चलता है, संदर्भ स्विच की विलंबता कई माइक्रोसेकंड के रूप में अनुमानित किया जा सकता है। आइए 10 को ऊपरी सीमा के रूप में लें। तो, 10K स्विच एक साथ लगभग 100ms, या 25ms प्रति CPU लेंगे। यह संभावना नहीं है कि आपका परीक्षण इस ओवरहेड का पता लगाएगा। इसके अलावा, सभी धागे विशुद्ध रूप से सीपीयू से बंधे होते हैं - वे सीपीयू कैश प्रदूषण से पीड़ित होने के लिए पर्याप्त मेमोरी तक नहीं पहुंचते हैं। वे अन्य साझा संसाधनों तक भी नहीं पहुंचते हैं, इसलिए इस मामले में कोई अप्रत्यक्ष संदर्भ स्विच ओवरहेड नहीं है।

3
apangin 19 पद 2020, 19:19