2012-01-30 16 views
7

मैं एक गणना-गहन एल्गोरिदम अनुकूलित करने की कोशिश कर रहा हूं और कुछ कैश समस्या पर फंस गया हूं। मेरे पास एक बड़ा बफर है जो कभी-कभी और यादृच्छिक रूप से लिखा जाता है और एप्लिकेशन के अंत में केवल एक बार पढ़ा जाता है। जाहिर है, बफर में लिखने से बहुत सारे कैश मिस पैदा होते हैं और कैशों को प्रदूषित करने के बाद बाद में गणना के लिए जरूरी होता है। मैंने गैर-अस्थायी चाल instrinsics का उपयोग करने की कोशिश की, लेकिन कैश याद आती है (valgrind द्वारा रिपोर्ट और रनटाइम माप द्वारा समर्थित) अभी भी होता है। हालांकि, गैर-अस्थायी चालों की और जांच करने के लिए, मैंने एक छोटा परीक्षण कार्यक्रम लिखा, जिसे आप नीचे देख सकते हैं। अनुक्रमिक पहुंच, बड़े बफर, केवल लिखते हैं।_mm_stream_ps एल 1/एलएल कैश का उत्पादन क्यों करता है?

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#include <smmintrin.h> 

void tim(const char *name, void (*func)()) { 
    struct timespec t1, t2; 
    clock_gettime(CLOCK_REALTIME, &t1); 
    func(); 
    clock_gettime(CLOCK_REALTIME, &t2); 
    printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec)/1000000000); 
} 

const int CACHE_LINE = 64; 
const int FACTOR = 1024; 
float *arr; 
int length; 

void func1() { 
    for(int i = 0; i < length; i++) { 
     arr[i] = 5.0f; 
    } 
} 

void func2() { 
    for(int i = 0; i < length; i += 4) { 
     arr[i] = 5.0f; 
     arr[i+1] = 5.0f; 
     arr[i+2] = 5.0f; 
     arr[i+3] = 5.0f; 
    } 
} 

void func3() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 4) { 
     _mm_stream_ps(&arr[i], buf); 
    } 
} 

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[4], buf); 
     _mm_stream_ps(&arr[8], buf); 
     _mm_stream_ps(&arr[12], buf); 
    } 
} 

int main() { 
    length = CACHE_LINE * FACTOR * FACTOR; 

    arr = malloc(length * sizeof(float)); 
    tim("func1", func1); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func2", func2); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func3", func3); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func4", func4); 
    free(arr); 

    return 0; 
} 

फ़ंक्शन 1 बेवकूफ दृष्टिकोण है, फ़ंक्शन 2 लूप अनोलिंग का उपयोग करता है। फंक्शन 3 movntps का उपयोग करता है, जो वास्तव में कम से कम असेंबली में डाला गया था जब मैंने -O0 की जांच की थी। फंक्शन 4 में मैंने CPU को लिखने में मदद करने के लिए एक बार में कई movntps निर्देश जारी करने का प्रयास किया। मैंने gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c के साथ कोड संकलित किया जहां X [0..3] में से एक है। परिणाम हैं .. कहने के लिए दिलचस्प पर सबसे अच्छा:

-O0 
func1 : 0.407794 s. 
func2 : 0.320891 s. 
func3 : 0.161100 s. 
func4 : 0.401755 s. 
-O1 
func1 : 0.194339 s. 
func2 : 0.182536 s. 
func3 : 0.101712 s. 
func4 : 0.383367 s. 
-O2 
func1 : 0.108488 s. 
func2 : 0.088826 s. 
func3 : 0.101377 s. 
func4 : 0.384106 s. 
-O3 
func1 : 0.078406 s. 
func2 : 0.084927 s. 
func3 : 0.102301 s. 
func4 : 0.383366 s. 

आप _mm_stream_ps देख सकते हैं दूसरों जब कार्यक्रम जीसीसी से अनुकूल नहीं है लेकिन फिर काफी अपने उद्देश्य में विफल रहता है जब जीसीसी अनुकूलन चालू है की तुलना में थोड़ा तेज है । Valgrind अभी भी बहुत सारे कैश लिखने की यादें रिपोर्ट करता है।

तो, प्रश्न हैं: क्यों वे (एल 1 + एलएल) कैश याद करते हैं, भले ही मैं एनटीए स्ट्रीमिंग निर्देशों का उपयोग कर रहा हूं? विशेष रूप से func4 इतनी धीमी क्यों है ?! क्या कोई यहां बता रहा है कि क्या हो रहा है?

+2

यदि आप ऑप्टिमाइज़ेशन सक्षम के साथ संकलित कर रहे हैं तो आपको वास्तव में यह जानने के लिए असेंबली को देखना होगा कि क्या हो रहा है। – RussS

+0

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

उत्तर

8
  1. शायद, अपने बेंचमार्क उपायों ज्यादातर स्मृति आवंटन प्रदर्शन, केवल प्रदर्शन नहीं लिखा। आपका ओएस malloc में मेमोरी पेज आवंटित नहीं कर सकता है, लेकिन आपके func* फ़ंक्शंस के अंदर, पहले स्पर्श पर। बड़ी मात्रा में स्मृति आवंटित होने के बाद ओएस कुछ मेमोरी शफल भी कर सकता है, इसलिए मेमोरी आवंटन के बाद किए गए किसी भी मानक, विश्वसनीय नहीं हो सकते हैं।
  2. आपके कोड में aliasing समस्या है: संकलक गारंटी नहीं दे सकता कि आपके सरणी का सूचक इस सरणी को भरने की प्रक्रिया में नहीं बदलता है, इसलिए इसे हमेशा पंजीकरण का उपयोग करने के बजाय स्मृति से arr मान लोड करना होगा। इससे कुछ प्रदर्शन में कमी आ सकती है। एलियासिंग से बचने का सबसे आसान तरीका स्थानीय चर के लिए arr और length कॉपी करना है और सरणी को भरने के लिए केवल स्थानीय चर का उपयोग करना है। वैश्विक चर से बचने के लिए कई प्रसिद्ध सलाहएं हैं। एलिसिंग कारणों में से एक है।
  3. _mm_stream_ps सरणी को 64 बाइट्स द्वारा गठबंधन किया गया बेहतर काम करता है। आपके कोड में कोई संरेखण की गारंटी नहीं है (वास्तव में, malloc इसे 16 बाइट्स द्वारा संरेखित करता है)। यह अनुकूलन केवल छोटे सरणी के लिए ध्यान देने योग्य है।
  4. _mm_stream_ps के साथ समाप्त होने के बाद _mm_mfence पर कॉल करना एक अच्छा विचार है। प्रदर्शन के लिए नहीं, शुद्धता के लिए यह आवश्यक है।
+1

बहुत बहुत धन्यवाद Evgeny! 1. यह है। मुझे इसके बारे में पता नहीं था। जब मैंने केवल एक बार स्मृति आवंटित करने के लिए कोड बदल दिया, तो मैंने नाटकीय रूप से रनटाइम को बदल दिया जो मैंने शुरू में अपेक्षित था। Func3 + 4 func1 + 2 से 2-3x तेज है। 2. क्या आप इस पर थोड़ा और विस्तार कर सकते हैं? मैंने सोचा था कि एलियासिंग केवल वर्चुअल मेमोरी <-> भौतिक स्मृति के बारे में एक समस्या होगी। मुझे नहीं पता कि यह कहां एक मुद्दा है। 3. ठीक है, तो मुझे valloc() या कुछ अन्य libc विशिष्ट फ़ंक्शन का उपयोग करना होगा? रनटाइम पर कोई प्रभाव नहीं पड़ा। कैश लाइन संरेखण को सीपीयू को लिखने के संयोजन में मदद करनी चाहिए, क्या मैं सही हूँ? 4. ठीक है। –

+1

मैंने एलियासिंग के साथ-साथ विकिपीडिया लिंक के बारे में कुछ स्पष्टीकरण जोड़े। कैश लाइन संरेखण सरणी के पहले 64 बाइट्स के लिए सही संयोजन का उपयोग करने की अनुमति देता है। संरेखण के लिए आप कई मंच-निर्भर कार्यों का उपयोग कर सकते हैं, मुझे अभी उन सभी को याद नहीं है। या आप '(पी +63) और ~ 63' चाल का उपयोग कर सकते हैं। या अगर आपके सरणी हमेशा मेगाबाइट्स से बड़े होते हैं तो संरेखण को अनदेखा करें। –

+1

एलियासिंग मुद्दे के बारे में, आपको ग्लोबल्स के रूप में रखने के बजाय, अपने कार्यों के तर्क के रूप में 'arr' और 'length' को पास करने का प्रयास करना चाहिए। यह * कंपाइलर के लिए अनुकूलन के अवसरों में सुधार कर सकता है। – rotoglup

2

func4 इस होना नहीं चाहिए:

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[i+4], buf); 
     _mm_stream_ps(&arr[i+8], buf); 
     _mm_stream_ps(&arr[i+12], buf); 
    } 
} 
+0

आप सही हैं।धन्यवाद :-) यह func3 के समान परिणामों के बारे में func4 लेता है। –

संबंधित मुद्दे