Autor Beitrag
Niko S.
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 566
Erhaltene Danke: 10

Win 7, Ubuntu
Lazarus, Turbo Delphi, Delphu 7 PE
BeitragVerfasst: Do 18.02.10 22:54 
Ich hoffe das passt hier rein.
Es ist ein Topic aus einem anderen Forum, kann's leider nicht verlinken, da es ohne Account nicht einsehbar ist.
Daher quote ich das einfach mal. Vielleicht finden das auch andere komisch.
Ich finde der typ übertreibt es einfach.
Zitat:


Foreword: this topic is heavily biased, but with a reason, if you're weak-hearted then don't even consider reading this. If you think you should flame me or cause problems, then enjoy your warning.


Greetings,

Today and probably in the future I'll be posting some topics about the low-level world vs. the high-level world. Basically, I'll also start the first topic in the Programming Discussion board, but I'll also be starting the first topic of the series and I'll probably continue it as well.

Basically I've been using nothing else but Assembly for the last two months, basically for the x86(-64) architecture. I'm incredibly fluent in Assembly and if you don't believe me, ask Admiral Refuge. However, Assembly is not the only language I know and use, I also generally use C, C++, Java, BASIC and various other languages and I know how to read, interpret and even port most languages. Anyway, enough about me, lets start with the actual topic.

What am I going to cover in this article? Virtually, I'll be covering various languages, especially x86(-64) Assembly, C and C++ and I'll be discussing their performance, but also their standards. Some of us know that Assembly is the lowest-level of programming on the x86(-64) architecture next to actual machine code, which is basically the same, but in a binary form, which the processor can actually interpret and execute. C and C++ are also programming languages, but they're more high-level and thus more abstract than Assembly. Languages like C and C++ are more structural and contain more logical constructions than pure Assembly does. Also, they allow more abstraction than Assembly, especially C++, as C++ provides classes, enumerations, etc. and several design patterns come with them as well. However, all these neat features which should ease up your code have a price, and actually they have two prices.

For one everybody will agree that C and C++ will never be faster than Assembly. However, it's still possible to write code that is as fast as Assembly and then again, it isn't. I'll be showing this to you later on, I have some other things to mention first. The second price is that your code won't be easier at all. Think of it, what would be easier? Writing a simple shell script like MS-DOS and UNIX allow, or writing a shell script in C-syntax with preprocessing and everything? I think most people already just prefer to use shell scripts instead of makefiles, why would we even use C-syntax shell scripts then? Don't ask me, as I can't really answer that one. I can however answer the actual question: "Why do people prefer C/C++ (or high-level) over Assembly?" The answer is simple; most programmers can't code.

Lets clear that up. Why can't programmers code, according to me? It's quite simple. They can probably write stable and good working code, but most programmers fail at writing properly commented code, which is, in my opinion, a must for good code and most programmers who know Assembly fail at writing properly commented code. GRUB is an example of that, or just AT&T Assembly is. Most programmers who use Assembly just write code like this:

ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
main:
cli
ljmp $0, $l1
l1:
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movw $0x7C00, %sp
sti


That's just plainly horrible; it's void of comments and the AT&T is pretty ugly on itself. If you were to write a new language specification with a full programming toolkit for it and everything and if you used that as your promotion sample, then forget it, it won't work. Most C/C++ is beautiful either in most cases, but yet ugly C/C++ code doesn't look as worse as ugly Assembly code, otherwise nobody would be using WinAPI et al.

Anyway, enough about coding styles and habits. Lets talk about the standard of C and C++. Technically speaking it's one of the worst standards of all time. That is, the standard libraries provided with C and C++ miss the proper functionality they should have had like proper Unicode support, proper string classes and such things. C and C++ could have had a standard networking library as well, but either it's because of Microsoft or because most C compiler developers weren't interested into it that it doesn't exist. In short (or tl;dr): C/C++'s standard libraries are flawed.

Now, if you're one of the guys who thinks that .net is great at replacing C/C++ and the standard libraries, then GTFO. It isn't and it will never be as it is filled with failure at so many levels. If you feel the need for a virtual machine then you're currently best off with Java, especially since it has better standards than both .net and C/C++. The basic reason behind the fact that C/C++ doesn't have proper standards and specifications is due evolution anyway, that and companies like Microsoft who think they're the only people on this world.

However, enough with this talk 'n stuff. It's time to actually look at performance issues. I, for myself, can't even understand why C was actually made. I know it was intentionally made for the creation of the UNIX kernel, but then again, it is not such a great language for kernels at all, as it has many performance issues compared to Assembly. In this article I'll cover that using memcpy.

Lets actually implement a memcpy function in pure C and then actually port it to Assembly. So lets first start off with some C:

ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
void *memcpy(void* destination, const void* source, size_t size)
{
  /* As long as there are any bytes left to copy. */
  for (int index = 0; index < size; ++index)
  {
    /* Set the byte at the destination pointer incremented by the index to the byte at the source pointer incremented by the index.
    destination[i] = source[i];
  }
  
  /* Return the destination pointer. */
  return destination;
}


Awesome, now lets see how C usually make a call to the memcpy function:

ausblenden Quelltext
1:
2:
  /* Call memcpy with the imaginary parameters: dest, src and size. */
  memcpy(dest, src, size);


In Assembly the code would look like:

ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
  ; Push the third parameter on the stack.
  push DWORD [size]
  
  ; Push the second parameter on the stack.
  push DWORD [src]
  
  ; Push the first parameter on the stack.
  push DWORD [dest]
  
  ; Call _memcpy.
  call _memcpy
  
  ; Deallocate the used parameters.
  add esp, 12


Now lets port the memcpy function to x86 Assembly:

ausblenden volle Höhe Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
; The section below is written in 32-bits.
[BITS 32]

; The section below is .text.
[SECTION .text]

; Make _memcpy available to other object/source files.
[GLOBAL _memcpy]

; The memcpy function.
_memcpy:
  ; Store the base pointer on the stack.
  push ebp
  
  ; Set the base pointer to the stack pointer.
  mov ebp, esp
  
  ; Set the destination index to first parameter, the destination.
  mov edi, DWORD [ebp + 8]
  
  ; Set the source index to the second parameter, the source.
  mov esi, DWORD [ebp + 12]
  
  ; Nullify the C-register.
  xor ecx, ecx
  
.CopyByte:
  ; Is the C-register greater than or equal to the third parameter?
  cmp ecx, DWORD [ebp + 16]
  
  ; It is, we're done.
  jae .Done
  
  ; Set the A-register to the byte at the source index incremented by the C-register, the index.
  mov al, BYTE [esi + ecx]
  
  ; Set the byte at the destination index incremented by the C-register, the index, to the A-register.
  mov BYTE [edi + ecx], al
  
  ; Increment the C-register.
  inc ecx

  ; Copy the next byte.
  jmp .CopyByte
  
.Done:
  ; Set the A-register to the first parameter as that is the initial destination index.
  mov eax, DWORD [ebp + 8]
  
  ; Set the stack pointer to the base pointer.
  mov esp, ebp
  
  ; Restore the base pointer.
  pop ebp
  
  ; Return.
  ret


All the mess with the base pointer is actually part of the cdecl ABI. This code might look great, but it's the worst optimised Assembly around, but it's probably the best you could get in just C. If you're lucky a formal compiler would leave out the cdecl-specific stuff as it isn't required in this case. So you'd end up with:

ausblenden volle Höhe Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
; The section below is written in 32-bits.
[BITS 32]

; The section below is .text.
[SECTION .text]

; Make _memcpy available to other object/source files.
[GLOBAL _memcpy]

; The memcpy function.
_memcpy:
  ; Set the destination index to first parameter, the destination.
  mov edi, DWORD [esp + 8]
  
  ; Set the source index to the second parameter, the source.
  mov esi, DWORD [esp + 12]
  
  ; Nullify the C-register.
  xor ecx, ecx
  
.CopyByte:
  ; Is the C-register greater than or equal to the third parameter?
  cmp ecx, DWORD [esp + 16]
  
  ; It is, we're done.
  jae .Done
  
  ; Set the A-register to the byte at the source index incremented by the C-register, the index.
  mov al, BYTE [esi + ecx]
  
  ; Set the byte at the destination index incremented by the C-register, the index, to the A-register.
  mov BYTE [edi + ecx], al
  
  ; Increment the C-register.
  inc ecx

  ; Copy the next byte.
  jmp .CopyByte
  
.Done:
  ; Set the A-register to the first parameter as that is the initial destination index.
  mov eax, DWORD [esp + 8]
  
  ; Return.
  ret


We could actually optimise it in Assembly and that's why libc is or should be implemented in Assembly and not in C. The x86(-64) architecture allows the use of string instructions which are incredibly useful for things like data transfer. If you know a bit about Assembly, you might have heard about lods and stos, but those instructions wouldn't be a good idea at all. It's better to use movs for this job. So lets optimise it:

ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
; The section below is written in 32-bits.
[BITS 32]

; The section below is .text.
[SECTION .text]

; Make _memcpy available to other object/source files.
[GLOBAL _memcpy]

; The memcpy function.
_memcpy:
  ; Set the destination index to first parameter, the destination.
  mov edi, DWORD [esp + 8]
  
  ; Set the source index to the second parameter, the source.
  mov esi, DWORD [esp + 12]
  
  ; Set the C-register to the third parameter, the size.
  mov ecx, DWORD [esp + 16]
  
  ; Execute the 8-bit data transfer and copy an amount, specified in the C-register, of bytes from the source index to the destination index.
  rep movsb
  
  ; Set the A-register to the first parameter as that is the initial destination index.
  mov eax, DWORD [esp + 8]
  
  ; Return.
  ret


This code alone would be incredibly fast already, but there's still some optimisation left to do. The 8-bit transfer should actually be 32-bit, but how are we going to do that when the size is specified in bytes and not in double words (a.k.a. 32-bit integers)? It's quite simple actually. We just convert the amount of bytes to the amount of double words like in the sample below:

ausblenden volle Höhe Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
; The section below is written in 32-bits.
[BITS 32]

; The section below is .text.
[SECTION .text]

; Make _memcpy available to other object/source files.
[GLOBAL _memcpy]

; The memcpy function.
_memcpy:
  ; Set the destination index to first parameter, the destination.
  mov edi, DWORD [esp + 8]
  
  ; Set the source index to the second parameter, the source.
  mov esi, DWORD [esp + 12]
  
  ; Set the C-register to the third parameter, the size.
  mov ecx, DWORD [esp + 16]
  
  ; Add three the C-register.
  add ecx, 3
  
  ; Nullify the first two bits in the C-register.
  and ecx, 11111111111111111111111111111100b
  
  ; Right shift the C-register by two bits, thus divide the C-register by four.
  shr ecx, 2
  
  ; Execute the 32-bit data transfer and copy an amount, specified in the C-register, of bytes from the source index to the destination index.
  rep movsd
  
  ; Set the A-register to the first parameter as that is the initial destination index.
  mov eax, DWORD [esp + 8]
  
  ; Return.
  ret


That would be the most optimised memcpy function available for C, unless you'd be using something like SIMD extensions or something similar. Here's the 64-bit memcpy as well:

ausblenden volle Höhe Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
; The section below is written in 64-bits.
[BITS 64]

; The section below is .text.
[SECTION .text]

; Make _memcpy available to other object/source files.
[GLOBAL _memcpy]

; The memcpy function.
_memcpy:
  ; Set the C-register to the D-register, which is the third parameter, which is the size.
  mov rcx, rdx
  
  ; Set the A-register, the first return value, to the destination index, which is the first parameter, which is the destination.
  mov rax, rdi
  
  ; Add seven the C-register.
  add rcx, 7
  
  ; Nullify the first four bits in the C-register.
  and rcx, 1111111111111111111111111111111111111111111111111111111111110000b
  
  ; Right shift the C-register by three bits, thus divide the C-register by eight.
  shr ecx, 3
  
  ; Execute the 64-bit data transfer and copy an amount, specified in the C-register, of bytes from the source index to the destination index.
  rep movsq
  
  ; Nullify the D-register, the second return value.
  xor rdx, rdx
  
  ; Return.
  ret


That's all. There are now two fully optimised memcpy functions, one for the x86 architecture and the other for the x86-64 architecture. However, it would still be slightly slower than Assembly as you could just set the C-register, the destination index, the source index and use rep movsd/movsq. These functions would however be incredibly fast compared to pure C/C++ and that's why (inline) Assembly should be used for some/most things instead of C/C++ or even Java or the .net framework.


Regards,
Godlord.


Nun exisitieren noch eine ganze reihe weiterer Topics.
In dem einen hat er z.b. jemanden vollkommen fertig gemacht, weil er eine 2D Spieleentwicklungs Engine (Also sowas wie der RPG XP nur halt in MMORPG version) in C# programmieren wollte.
F34r0fTh3D4rk
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 5284
Erhaltene Danke: 27

Win Vista (32), Win 7 (64)
Eclipse, SciTE, Lazarus
BeitragVerfasst: Do 25.02.10 00:38 
Es macht macht viel mehr Sinn, sich um Dinge wie Laufzeiten, als um solchen Kleinkram zu kümmern. Was nützt es mir, wenn ich ultra-schnellen Assembler Code habe, aber mein Code in exponentieller Laufzeit läuft, wenn es doch in O(n³) möglich ist?
Wenn man wirklich Hardware (also Mikrokontroller) programmiert, macht eine solche Optimierung vielleicht schon eher Sinn, zumal man auch den Platz bedenken muss.

Er hat sich halt viel mit ASM beschäftigt und hat es auch richtig drauf und will deshalb nicht wahrhaben, dass es eigentlich doch Quatsch ist ;)