الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

النسخ من الذاكره

بدأه C++er في 10 يونيو 2010 · 16 رد · 1,540 مشاهدة · في لغة C و ++C
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

إذا كنت مثلى مهووس بأداء البرنامج و تقوم بعمل benchmarking بالمللى ثانيه ستعرف أن الداله memcpy لا تصلح ابدا مع عمليات نسخ المصفوفات كبيرة الحجم (و التى تقارب 20 ميجا بايت) حيث وقتها سرعة اداء الداله ستكون ضعيفه و ايضا ستقاس بالثانيه.

داخل العدبد من الـ architecture تجد مصنعيها يقوموا بإضافة دعم النسخ بإستخدام الـ SIMD و ذلك لسرعتها فى الأداء (بالإضافة لوجود الكاش) و من هؤلاء المصنعين Intel و AMD و POWER PC و SPARC و MIPS (هؤلاء هم ما اعرف) و غيرهم.

داخل نظام تشغيل ويندوز توجد نسختان للداله memcpy احداهما تستخدم مع نسخ الـ Debug و هى مكتوبة بلغة الـ C++ لتتيح لك امكانية تنقيح البرنامج و اثناء الـ Release يتم ابدالها بنسخه مكتوبة بالأسمبلى من Intel و هذا هو شكل الإثنان:

نسخة الـ Debug

void * memcpy (void * dst, const void * src, size_t count)
{
        void * ret = dst;

        while (count--) {
                *(char *)dst = *(char *)src;
                dst = (char *)dst + 1;
                src = (char *)src + 1;
        }

        return(ret);
}

نسخة الـ Release و تستطيع ايجاد الكود الخاص بها داخل مجلد crt\src\intel الموجود بمسار تثبيت فيجوال سى و الملف اسمه memcpy.asm

و صدقونى سواء كود انتل او ميكروسوفت فالإثنان على درجة واحده من السرعه (كود انتل يقوم بعمل align للذاكره قبل النسخ لجعله أسرع و لكن مع ذلك ايضا النسخ يتم بايت بايت).

توجد اساليب افضل للنسخ اكثر من بايت من الذاكره مثل استخدام rep movsw و التى تنسخ 2 بايت مع كل 4 cycle أو rep movsd و التى تنسخ 4 بايت مع كل 4 cycle و كلاهم اسرع كثيرا من كود انتل و ميكروسوفت. (عدد الـ cycles يختلف من جهاز لأخر تبعا لإصدار المعالج)

إذا ذهبنا إلى PowerPC (الإصدار 440 و 464) سنجد ان نسخ memcpy يشبه كثيرا ما وجدناه مع ميكروسوفت و أنتل و لكن IBM قامت بإصدار Application Note عام 2008 تشرح فيها استخدام كود الأسمبلى لنسخ ما يقارب 64 ميجا بأقل وقت ممكن (من 1000 إلى 1200 ميللى ثانيه).

حتى الأن لازلت اقرأ عن SPARC و MIPS و لا اعرف كيف يتم النسخ بهم.

بالنسبه للـ Application Note الخاصه بـ PowerPC يمكنك تحميله من هنــا

بالنسبه لإنتل فقد قمت بكتابة داله تقوم بالتحقق من امكانيات البروسسور فإن كان SSE متاح فهى تقوم بنسخ 16 بايت كدفعه واحده و إذا كان SSE غير متاح و MMX متاح فيتم استخدامها و يتم نسخ 16 بايت دفعه واحده (استخدم مسجلين mm0 و mm1 لإتمام عملية النسخ) و إذا كان كلاهما غير مدعوم فتوجد حالتين إن كان البروسسور خاصتك هو 32 بت فيتم استخدم rep movsd لنسخ 4 بايت كدفعه واحده و إن كان 64 بت فيتم استخدام rep movsq لنسخ 8 بايت دفعه واحده.

سأترك لك عمل الـ benchmarking على جهازك فجهازى قديم بالكد يدعم SSE2.

بالنسبه للكود فهو حتى الأن يدعم Intel 32 و Intel 64 فقط قم بتعريف الماكرو STRLIB_MEM_64 و ذلك حتى تستخدم كود 64 بت إذا كان البروسسور خاصتك داعم له، فى الواقع هذه الداله قمت بتصميمها خصيصا لمكتبتى StrLib حتى يكون نسخ النصوص من الذاكره اسرع ما يمكن.

توجد تحسينات و اضافات سيتم إلحاقها بهذا الكود فيما بعد مثل دعم مترجم GCC و كذلك اضافة دعم PPC 32bit و PPC 64bit، أترككم مع الكود

void strlib_memcpy(void* dst, const void* src, size_t cnt)
{
#ifndef STRLIB_MEM_64
	__asm
	{
		push esi
		push edi

		; move pointers to registers
		mov  esi, src   ; copy src value to esi
		mov  edi, dst   ; copy dest value to edi

		; check for CPUID support
		pushfd              ; save eflags
		pop	eax             ; restore eflags in eax
		btc	eax, 21         ; reverse bit 21
		push	eax         ; save eax
		popfd               ; pop eax to eflags
		pushfd              ; save eflags (bit 21 will be set to its true value)
		pop	edx             ; restore eflags in edx
		xor	eax, edx        ; check if bit 31 is changed
		jnz	COPY_WITH_MOVS  ; if CPUID not supported (bit 21 is zero) copy using movsd

		; check the feature
		mov eax, 1         ; set eax to 1
		cpuid              ; get cpu feature
		bt  edx, 25        ; check for SSE support
		jc  COPY_WITH_SSE  ; copy using SSE if available
		bt  edx, 23        ; check for MMX support
		jc  COPY_WITH_MMX  ; copy using MMX if available
		jmp COPY_WITH_MOVS ; copy using movsd

COPY_WITH_SSE:
		mov  ecx, cnt ; copy number of bytes
		mov  eax, ecx   ; copy ecx to eax
		shr  ecx, 4     ; divide ecx by 16
		shl  ecx, 4     ; multiply ecx by 16
		sub  eax, ecx   ; get number of bytes will remain after copy
		shr  ecx, 4     ; divide ecx by 16

		emms                  ; reset the MMX states
LP1:
		movdqu  xmm0,  [esi]  ; copy 16 bytes from src
		movdqu  [edi], xmm0   ; copy 16 bytes back to dest
		add     esi, 16       ; increase src pointer
		add     edi, 16       ; increase dest pointer
		dec     ecx           ; decrement ecx
		jnz     LP1          ; repeat as long as ecx not zero
		emms                  ; reset the MMX states
		mov     ecx, eax      ; copy remain bytes to ecx
		jmp     REMAINS       ; jump to copy remain bytes

COPY_WITH_MMX:
		mov  ecx, cnt ; copy number of bytes
		mov  eax, ecx   ; copy ecx to eax
		shr  ecx, 4     ; divide ecx by 16
		shl  ecx, 4     ; multiply ecx by 16
		sub  eax, ecx   ; get number of bytes will remain after copy
		shr  ecx, 4     ; divide ecx by 16

		emms                ; reset the MMX states
LP2:
		movq  mm0, [esi]    ; copy first 8 bytes from src
		movq  mm1, [esi+8]  ; copy next 8 bytes from src
		movq  [edi]  , mm0  ; copy first 8 bytes back to dest
		movq  [edi+8], mm1  ; copy next 8 bytes back to dest
		add   esi, 16     ; increase src pointer
		add   edi, 16     ; increase dest pointer
		dec   ecx         ; decrement ecx
		jnz   LP2         ; repeat as long as ecx not zero
		emms              ; reset the MMX states
		mov   ecx, eax    ; copy remain bytes to ecx
		jmp   remains     ; jump to copy remain bytes

COPY_WITH_MOVS:
		mov  ecx, cnt ; copy number of bytes
		mov  eax, ecx   ; copy ecx to eax
		shr  ecx, 2     ; divide ecx by 4
		shl  ecx, 2     ; multiply ecx by 4
		sub  eax, ecx   ; get number of bytes will remain after copy
		shr  ecx, 2     ; divide ecx by 4

		rep  movsd     ; repeat copy 4 bytes as long as ecx not zero
		mov  ecx, eax  ; copy remain bytes to ecx

REMAINS:
		rep movsb  ; copy remain bytes

		pop edi
		pop esi
	}
#else
	__asm
	{
		push rsi
		push rdi

		; move pointers to registers
		mov  rsi, src   ; copy src value to rsi
		mov  rdi, dst   ; copy dest value to rdi

		; CPUID is supported by default
		; check the feature
		mov eax, 1         ; set eax to 1
		cpuid              ; get cpu feature
		bt  edx, 25        ; check for SSE support
		jc  COPY_WITH_SSE  ; copy using SSE if available
		bt  edx, 23        ; check for MMX support
		jc  COPY_WITH_MMX  ; copy using MMX if available
		jmp COPY_WITH_MOVS ; copy using movsd

COPY_WITH_SSE:
		mov  ecx, cnt   ; copy number of bytes
		mov  eax, ecx   ; copy ecx to eax
		shr  ecx, 4     ; divide ecx by 16
		shl  ecx, 4     ; multiply ecx by 16
		sub  eax, ecx   ; get number of bytes will remain after copy
		shr  ecx, 4     ; divide ecx by 16

		emms                  ; reset the MMX states
LP1:
		movdqu  xmm0,  [rsi]  ; copy 16 bytes from src
		movdqu  [rdi], xmm0   ; copy 16 bytes back to dest
		add     rsi, 16       ; increase src pointer
		add     rdi, 16       ; increase dest pointer
		dec     ecx           ; decrement ecx
		jnz     LP1           ; repeat as long as ecx not zero
		emms                  ; reset the MMX states
		mov     ecx, eax      ; copy remain bytes to ecx
		jmp     REMAINS       ; jump to copy remain bytes

COPY_WITH_MMX:
		mov  ecx, cnt   ; copy number of bytes
		mov  eax, ecx   ; copy ecx to eax
		shr  ecx, 4     ; divide ecx by 16
		shl  ecx, 4     ; multiply ecx by 16
		sub  eax, ecx   ; get number of bytes will remain after copy
		shr  ecx, 4     ; divide ecx by 16

		emms                ; reset the MMX states
LP2:
		movq  mm0, [rsi]    ; copy first 8 bytes from src
		movq  mm1, [rsi+8]  ; copy next 8 bytes from src
		movq  [rdi]  , mm0  ; copy first 8 bytes back to dest
		movq  [rdi+8], mm1  ; copy next 8 bytes back to dest
		add   rsi, 16     ; increase src pointer
		add   rdi, 16     ; increase dest pointer
		dec   ecx         ; decrement ecx
		jnz   LP2         ; repeat as long as ecx not zero
		emms              ; reset the MMX states
		mov   ecx, eax    ; copy remain bytes to ecx
		jmp   remains     ; jump to copy remain bytes

COPY_WITH_MOVS:
		mov  ecx, cnt ; copy number of bytes
		mov  eax, ecx   ; copy ecx to eax
		shr  ecx, 3     ; divide ecx by 8
		shl  ecx, 3     ; multiply ecx by 8
		sub  eax, ecx   ; get number of bytes will remain after copy
		shr  ecx, 3     ; divide ecx by 8

		rep  movsd     ; repeat copy 8 bytes as long as ecx not zero
		mov  ecx, eax  ; copy remain bytes to ecx

REMAINS:
		rep movsb  ; copy remain bytes

		pop rdi
		pop rsi
	}
#endif
}

و الله ولى التوفيق

تم تعديل هذه المشاركة بواسطة Muhammad alaa في 10 يونيو 2010 في 03:18

5

مدونتي: C++ Tips and Tricks

#2

درس جميل, مع إني لم أفهم معظمه :lol:

#3

موضوع رائع .

هل الكود الناتج محمول ؟ باعتقادى انة فقط يستغل امكانيات المعالج الذى ترجم علية وليس باقى المنصات .

فى تلك الحالة هو رائع لهواة البناء من المصدر .

name : mohamedyosry

#4

الكود يعمل (للوقت الحالى فقط) على معالجات Intel و AMD و اعتقد VIA إذا كانت تدعم SSE و MMX (لم اجد كتب تشرح معالجاتها)، لذا يمكن القول ان الكود السابق يعمل فقط لمنصات CISC سواء كانت 32bit او 64bit مع مترجم MSVC و Intel و بالنسبه للـ GCC اقوم بتحويل الكود حاليا إليه و ذلك لأنه يستخدم AT&T Syntax و ليس Intel Syntax.

اقتباس
باعتقادى انة فقط يستغل امكانيات المعالج الذى ترجم علية وليس باقى المنصات

لا هو ليس كذلك، الكود يستغل امكانيات المعالج المتاحه و قت التشغيل و حيث ان عملية اختبار امكانيات المعالج تتم وقت التشغيل لذا الكود يعمل على قدر امكانيات المعالج الذى يتم تنفيذ الكود عليه، ألم أقل ..

اقتباس
سأترك لك عمل الـ benchmarking على جهازك فجهازى قديم بالكاد يدعم SSE2.

السبب فى عدم وضعى للـ Benchmarking هو أن المعالج الذى لدى قديم نسبيا "Pentium D" و بالتالى إذا كان لديك i3 أو i5 أو i7 ستظهر الإمكانيات الحقيقيه للكود.

اقتباس
درس جميل, مع إني لم أفهم معظمه :lol:

لا عليك، كنت بفكر اعمل تصدير للكود ده للـ JVM عن طريق JNI بس انا لسه خبرتى فى الموضوع ده صغيره، إن شاء الله لما اتعلمه كويس و اخلص كود RISC ابقى اشوف موضوع JNI.

و الله ولى التوفيق

1

مدونتي: C++ Tips and Tricks

#5

اة صحيح

بس عشان ال directive شككتنى فى الموضوع فقلت اتاكد (تقريبا كلامى لحد ما صح على استغلال ال64 بت )

name : mohamedyosry

#6

جميل أخي محمد :)

من الأفضل فصل كود التحقق من وجود الـinstructions عن كود النسخ واستدعاؤه في الـstartup فقط .

mov eax, dword ptr ds:[0xffdf0308]

jmp dword ptr [eax+0xfc]

#7
اقتباس
من الأفضل فصل كود التحقق من وجود الـinstructions عن كود النسخ واستدعاؤه في الـstartup فقط .

هل تقصد و ضعه فى إجراء فرعى و استدعائه من داخل دالة النسخ؟؟

انا كنت اريد فصل كود التحقق من وجود CPUID و لكنى لم اعرف اين سأقوم بوضعه خصوصا إذا كان الكود سيعمل على أكثر من platform.

مدونتي: C++ Tips and Tricks

#8
اقتباس
هل تقصد و ضعه فى إجراء فرعى و استدعائه من داخل دالة النسخ؟؟

لا فلا فائدة من ذلك ، اقصد فصله في اجراء فرعي واستدعاؤه مرة واحدة فقط اثناء الـstartup (مثل الـmain) .

شئ مثل هذا :

typedef void (*STRLIB_MEMCPY)(void *, void *, size_t);

STRLIB_MEMCPY strlib_memcpy = memcpy; // standard memcpy


void sse_memcpy(void *dest, void *src, size_t count)
{
	// sse code
}

void mmx_memcpy(void *dest, void *src, size_t count)
{
	// mmx code
}

void initmemcpy()
{
	check_available_instructions();
	if (sse)
		strlib_memcpy = sse_memcpy;
	else if (mmx)
		strlib_memcpy = mmx_memcpy;
}

void main()
{
	initmemcpy();
	strlib_memcpy(xxx, yyy, zzz);
}

تم تعديل هذه المشاركة بواسطة GamingMasteR في 10 يونيو 2010 في 09:08

mov eax, dword ptr ds:[0xffdf0308]

jmp dword ptr [eax+0xfc]

#9

بعد كل الـ AND والـ OR اللي بينا يا محمد تروح ناشر الموضوع في قسم السي :sleep:

فعلاً دنيا ملهاش أمان :resentful:

2

Do as I say, not as I do

We are Anonymous. We are Legion. We don't forgive. We don't forget

#10

ايوة ماهو القسم بتاعه :D

mov eax, dword ptr ds:[0xffdf0308]

jmp dword ptr [eax+0xfc]

#11
GamingMasteR كتب:

ايوة ماهو القسم بتاعه :D

وانت بتأيده بالكلام ده؟

ماشي يا دفعة 041.gif

Do as I say, not as I do

We are Anonymous. We are Legion. We don't forgive. We don't forget

#12

عيب عليك ، انت عارف اني اسمبلاوي حتى النخاع :wink:

mov eax, dword ptr ds:[0xffdf0308]

jmp dword ptr [eax+0xfc]

#13

السلام عليكم ...

رائع يا محمد, نريد benchmarking :wink:

#14

أيه يا G انت بتهدى النفوس و لا ايه :P

ايه يا X الكلام ده انت عارف ان قسم الأسمبلى و قسم السى اخوات و طبعا بين الإخوات مفيش فرق، بلاش الحساسيه البرمجيه :)

***************

typedef void (*STRLIB_MEMCPY)(void *, void *, size_t);

STRLIB_MEMCPY strlib_memcpy = memcpy; // standard memcpy


void sse_memcpy(void *dest, void *src, size_t count)
{
        // sse code
}

void mmx_memcpy(void *dest, void *src, size_t count)
{
        // mmx code
}

void initmemcpy()
{
        check_available_instructions();
        if (sse)
                strlib_memcpy = sse_memcpy;
        else if (mmx)
                strlib_memcpy = mmx_memcpy;
}

void main()
{
        initmemcpy();
        strlib_memcpy(xxx, yyy, zzz%2
;
}

عارف هى فكره حلوه بس عيبها موضوع استدعاء initmemcpy عند الـ startup بس قياسا بكفاءة الكود بعد ذلك اعتقد انه لا غبار عليها، بالطريقه دى الكود اللى هيتم تنفيذه اقل و بالتالى هيكون اسرع.

و بنفس الطريقه اعمل implement لدالة الـ POWER PC و بإستخدام الماكرو نقدر نوصل للنسخه المستخدمه و يبقى الكود عند المستخدم النهائى له الشكل التالى:

void main()
{
        initmemcpy();
        strlib_memcpy(xxx, yyy, zzz);
}

فكرة جميله يا جى همخمخ لها كويس و بعد كده اطبقها بإذن الله.

***********************

اقتباس
رائع يا محمد, نريد benchmarking :wink:

جارى تجهيز كود الـ benchmarking

و الله ولي التوفيق

تم تعديل هذه المشاركة بواسطة Muhammad alaa في 10 يونيو 2010 في 18:45

مدونتي: C++ Tips and Tricks

#15

قمت بإضافة بعض التحسينات على الأكواد و كذلك قمت بتطبيق فكرة الأخ GamingMasteR على الكود (و هى تقسيمه لزيادة كفائته)، ايضا قمت بعمل كود للـ benchmark.

النسخه الجديده: mem_cpy.zip

كود قياس الكفاءة mainp.zip

داخل النسخه الجديده يوجد مايلى:

قم بتعريف الماكرو STRLIB_REPLACE_MEMCPY لإستبدال نسخة memcpy بما يتناسب مع الجهاز الحالى (الشرح لاحقا)

استخدم strlib_memcpy لإجراء عملية النسخ و هى مؤشر للداله التى ستقوم بالنسخ

قم بإستدعاء الااله init_memcpy قبل اجراء اى عملية نسخ بإستخدام strlib_memcpy (مرة واحده فقط) لإنها هى ما تقوم بتحديد اى دوال النسخ التالى ذكرها افضل للجهاز الحالى.

استخدم sse_memcpy لتقوم بالنسخ بإستخدام sse

استخدم mmx_memcpy لتقوم بالنسخ بإستخدام mmx

استخدم movs_memcpy لتقوم بالنسخ بإستخدام movsd

ملف الـ benchmark يقوم بمعرفة اى امكانيات البروسسور متاحه بجهازك و يقوم بعمل اختبار لهم جميعا (ما يتعلق بالنسخ فقط) و يظهر لك النتائج النهائيه بالملى ثانيه.

مساحة النسخ التى يتم الإختبار عليها يتم تخزينها بالماكرو MEMORY_SIZE و المساحه تكتب بالميجا بايت و يتم حجز ضعفها واحد للـ src و الأخر للـ dst.

لا تقم بإستخدام الدوال sse_memcpy و mmx_memcpy مباشرة إلا إذا كان البرسسور داعم للتعليمات الخاصه بهم.

و الله ولى التوفيق

تم تعديل هذه المشاركة بواسطة Muhammad alaa في 11 يونيو 2010 في 05:15

1

مدونتي: C++ Tips and Tricks

#16

السلام عليكم ...

شكراً جزيلاً أخي محمد على توفير الـ benchmarking. هناك عدة نقاط, ربما تكون مفاجئة و لكن أعتقد أنك تستطيع إصلاحها.

في جميع التجارب التي قمت بها, على أحجام مختلفة من الذاكرة, كان:

MOVS هو الأسرع مع تقارب في النتيجة مع memcpy.

MMX كان أسرع من SSE, و الغريب أن SSE كان الأبطأ دائماً.

أعمل على Win 7 x64, و معالجي هو Q6600.

لو عملت على تحري السبب, ربما ستجده فأنت أعلم مني في معالجات Intel, خبرتي معدومة للأسف فيها. و لكن أعتقد أنك تقوم بعملية النسخ قبل التأكد من أن العنوان aligned على 16 bytes مما يجبر المعالج على القيام بذلك, و لهذا يظهر SSE على أنه الأبطأ. لست أهلاً لقراءة الكود, و لكن هل تقوم ببدء عملية النسخ من aligned address؟

تحياتي...

تم تعديل هذه المشاركة بواسطة Khaled.Alshaya في 11 يونيو 2010 في 05:25

1
#17

شكرا اخى خالد على الـ Feedback :)

لاحظت ايضا بطء فى عمل SSE و ذلك يرجع إلى أنى استخدم مسجل واحد فقط من الـ 8 المتاحين فى حين تجد أن MMX اسرع منها (إن لم تكن اسرعهم على الإطلاق) و ذلك لأنى استخدم مسجلين دفعه واحده.

بعد عدة اختبارات و مقارنة النتائج و جدت بعض الحلول التى إن شاء الله ستقوم بمضاعفة أداء نسخة sse و mmx إلى 4 او 5 اضعاف و ذلك بإستخدام مسجلات اكثر مع الكاش و اتباع اسلوب برمجى افضل من الحالى.

بالنسبه للداله sse_memcopy فتوجد لها حالتان و هما إما ان يكون الـ src و الـ dst هم aligned و بذلك استخدم تعليمات النسخ للـ aligned memory و غير ذلك استخدم تعليمات الـ unaligned memory و هى ابطء بالتأكيد و لكن البطء سيكون فرق يقاس بالميللى ثانيه.

بالنسبه لـ mmx لا اقوم بالتحقق من الـ aligned و سأقوم فى النسخه القادمه، بالنسبه لنسخه movs لا دخل لى بها فهى حلقة داخليه فى المعالج.

النسخه القادمه إن شاء الله سيكون فيها التالى: (خلال 3 ايام إن شاء الله)

1 - إعادة برمجة نسخة SSE و MMX و استخدام اكبر عدد من المسجلات و كذلك تغيير الـ algorithm المتبعه و استخدام واحده اكثر كفاءة.

2 - اضافة دعم مترجم GCC (كود اسمبلى AT&T Syntax).

3 - اضافة دعم الـ 64bit.

فيما بعد

1 - اضافة دعم لمنصات PPC 32bit و PPC 64bit.

و الله و لى التوفيق

تم تعديل هذه المشاركة بواسطة Muhammad alaa في 11 يونيو 2010 في 05:57

مدونتي: C++ Tips and Tricks

مواضيع مشابهة