Showing posts with label Performance. Show all posts
Showing posts with label Performance. Show all posts

Sunday, October 7, 2018

HBF, experimental version

I've been playing for a while with the attribute [unsafe], the reason is that I use interfaces all over the code but I manage my own objects, so I don't need this COM overhead, I need basically interfaces behaving as clever pointers.

The game offers to all entities the following interface which returns many interfaces to handle any part of the game behaviour, like this nobody is aware of the implementations of the classes.

  IGameServices = interface
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetSession: ISession;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetEvents: IEvents;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetPlayers: IPlayers;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetCoins: ICoins;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetMap: IMap;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetDrawer: IGraphicDrawer;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetGraphicInfo: IGraphicInfo;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetLog: ILog;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetProfile: IProfile;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetFont: IFont;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetSmallFont: IFont;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetWindow: IWindow;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetLayer(aIndex: integer): ILayer;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetCameraHelper: ICameraHelper;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetHud: IHud;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetEffects: IEffects;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetSound: ISoundDispatcher;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetMusic: IMusic;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetFileSystem: IFileSystem;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetOptimizer: IOptimizer;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetFactory: ISpriteFactory;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetBack: IBackground;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetSettings: ISettings;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetHighscore: IHighscore;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetLevels: ILevels;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetVars: IMemVar;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetAchievements: IAchievements;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetRunes: IRunes;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetShop: IShopHandler;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetTimer: ICrono;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetInput: IInput;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetCurrentScreen: IScreen;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetTrigger: ITriggerDispatcher;
    {$IFDEF _UNSAFE_}[result: unsafe]{$ENDIF}function GetChat: IChat;
  end;


So a typical enemy would do GameServices.GetSound.Play() in order to play a sound.

GameSprite.pas.4293: begin
00A6BA50 53               push ebx
00A6BA51 56               push esi
00A6BA52 8BF2             mov esi,edx
00A6BA54 8BD8             mov ebx,eax
GameSprite.pas.4294: Exit(FGameServices);
00A6BA56 8BC6             mov eax,esi
00A6BA58 8B93A4030000     mov edx,[ebx+$000003a4]
00A6BA5E E8D5739AFF       call @IntfCopy
GameSprite.pas.4295: end;
00A6BA63 5E               pop esi
00A6BA64 5B               pop ebx

00A6BA65 C3               ret 

The classic IntfCopy() is there and of course their counterparts InfClear(). When you have a system which continuously passes interfaces then is quite an overhead.

In the experimental version we get:

GameSprite.pas.2834: result := GameServices.GetSound.Play(aFileName, aLoop, True);
00A63218 6A01             push $01
00A6321A 8BC6             mov eax,esi
00A6321C E863390000       call TGameSprite.GetGameServices

GameSprite.pas.4293: begin
00A66B84 51               push ecx
GameSprite.pas.4294: Exit(FGameServices);
00A66B85 8B80A4030000     mov eax,[eax+$000003a4]
00A66B8B 890424           mov [esp],eax
GameSprite.pas.4295: end;
00A66B8E 8B0424           mov eax,[esp]
00A66B91 5A               pop edx

00A66B92 C3               ret 

Hooray!, all that overhead is gone. The problem is that the compliler very often makes a mess and the code explodes as an AV (I haven't found a pattern), so I change the way the calls are done and then it no longer crashes.

For example this crashes (in some places of the code some others not):

GameServices.GetPlayers.Kill(pOne);

changing the code  a bit as:

players := GameServices.GetPlayers;
players.Kill(pOne);

Not a big deal but quite cumbersome to do all over the code, so I change it when it crashes.

Still not convinced?

Performance test with classic interfaces:



About 40 FPS for 50000 triangles.

Now with this UNSAFE stuff:



Almost 48 FPS for 50000 triangles.

I believe if worth the effort. It is important to note that interfaces should all be declared as const and never as inline.

So once I'm happy with the results I will publish this version.

Cheers.

Thursday, October 13, 2011

Reasons why ChessKISS 64 is slower than 32, part 2

 While debugging I've found this, quite interesting...

Search.pas.898: if board.LastPieceCaptured <> nil then //same value and index
0000000000615F3D 488B8338040000   mov rax,[rbx+$00000438]
0000000000615F44 90               nop
0000000000615F45 4885C0           test rax,rax
0000000000615F48 745D             jz $0000000000615fa7
Search.pas.900: recaptures[side] := board.LastPieceCaptured.GetMiddleValue + TMoveHelper.GetTo(aMove);
0000000000615F4A 488B8338040000   mov rax,[rbx+$00000438]

LastPieceCaptured is loaded into RAX, the pointer is valid, thus I'm going to use it, but again the compiler is loading the same value into the same register, what about the NOP operation in between?

Wednesday, October 12, 2011

Reasons why ChessKISS 64 is slower than 32

Optimized assembler in 32 bits mode:

Board.pas.1234: data.EnPassant := nil;
005412BF 8B45C4           mov eax,[ebp-$3c]
005412C2 33D2             xor edx,edx
005412C4 899008020000     mov [eax+$00000208],edx
Board.pas.1237: data.LastPieceCaptured := nil;
005412CA 8B45C4           mov eax,[ebp-$3c]
005412CD 33D2             xor edx,edx
005412CF 899024020000     mov [eax+$00000224],edx
Board.pas.1238: data.LastPieceMoved := nil;
005412D5 8B45C4           mov eax,[ebp-$3c]
005412D8 33D2             xor edx,edx
005412DA 899020020000     mov [eax+$00000220],edx
Board.pas.1239: data.LastMove := aMove;
005412E0 8B45C4           mov eax,[ebp-$3c]
005412E3 89B828020000     mov [eax+$00000228],edi
Board.pas.1240: piece := nil;
005412E9 33F6             xor esi,esi

Optimized assembler in 64 bits mode:

Board.pas.1234: data.EnPassant := nil;
00000000005E423F 488B8518030000   mov rax,[rbp+$00000318]
00000000005E4246 48C7800804000000000000 mov qword ptr [rax+$00000408],$0000000000000000
Board.pas.1237: data.LastPieceCaptured := nil;
00000000005E4251 488B8518030000   mov rax,[rbp+$00000318]
00000000005E4258 48C7803004000000000000 mov qword ptr [rax+$00000430],$0000000000000000
Board.pas.1238: data.LastPieceMoved := nil;
00000000005E4263 488B8518030000   mov rax,[rbp+$00000318]
00000000005E426A 48C7802804000000000000 mov qword ptr [rax+$00000428],$0000000000000000
Board.pas.1239: data.LastMove := aMove;
00000000005E4275 488B8518030000   mov rax,[rbp+$00000318]
00000000005E427C 8B8D50030000     mov ecx,[rbp+$00000350]
00000000005E4282 898838040000     mov [rax+$00000438],ecx

Both versions suffer from the same issue, loading into the same register the same value over and over (in EAX or RAX the compiler is loading the data variable), how can this still happening?

Monday, September 19, 2011

XE2, strange optimizations when in 64 bits...

 Given this silly code:

type
  TForm50 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    FA, FB, FC, FD: integer;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form50: TForm50;

implementation

{$R *.dfm}

procedure TForm50.FormCreate(Sender: TObject);
var
  i: integer;

begin
  FA := 0;
  FB := 0;
  FC := 0;
  FD := 0;

  for i := 0 to 999999 do
  begin
    Inc(FA);
    Inc(FB, 2);
    Inc(FC, 3);
    Inc(FD, 4);
  end;
end;

The optimized 32 bits code looks like:

Unit50.pas.31: FA := 0;
0050FE5C 33D2             xor edx,edx
0050FE5E 899090030000     mov [eax+$00000390],edx
Unit50.pas.32: FB := 0;
0050FE64 33D2             xor edx,edx
0050FE66 899094030000     mov [eax+$00000394],edx
Unit50.pas.33: FC := 0;
0050FE6C 33D2             xor edx,edx
0050FE6E 899098030000     mov [eax+$00000398],edx
Unit50.pas.34: FD := 0;
0050FE74 33D2             xor edx,edx
0050FE76 89909C030000     mov [eax+$0000039c],edx

Unit50.pas.36: for i := 0 to 999999 do
0050FE7C BA40420F00       mov edx,$000f4240
Unit50.pas.38: Inc(FA);
0050FE81 FF8090030000     inc dword ptr [eax+$00000390]
Unit50.pas.39: Inc(FB, 2);
0050FE87 83809403000002   add dword ptr [eax+$00000394],$02
Unit50.pas.40: Inc(FC, 3);
0050FE8E 83809803000003   add dword ptr [eax+$00000398],$03
Unit50.pas.41: Inc(FD, 4);
0050FE95 83809C03000004   add dword ptr [eax+$0000039c],$04
Unit50.pas.36: for i := 0 to 999999 do
0050FE9C 4A               dec edx
0050FE9D 75E2             jnz $0050fe81

Quite ok, direct access to the Self pointer located in EAX and decreasing loop.

and now the 64 bits version:

Unit50.pas.31: FA := 0;
000000000059C520 C7815006000000000000 mov [rcx+$00000650],$00000000
Unit50.pas.32: FB := 0;
000000000059C52A C7815406000000000000 mov [rcx+$00000654],$00000000
Unit50.pas.33: FC := 0;
000000000059C534 C7815806000000000000 mov [rcx+$00000658],$00000000
Unit50.pas.34: FD := 0;
000000000059C53E C7815C06000000000000 mov [rcx+$0000065c],$00000000
Unit50.pas.36: for i := 0 to 999999 do
000000000059C548 4833C0           xor rax,rax
Unit50.pas.38: Inc(FA);
000000000059C54B 488D9150060000   lea rdx,[rcx+$00000650]
000000000059C552 830201           add dword ptr [rdx],$01
Unit50.pas.39: Inc(FB, 2);
000000000059C555 488D9154060000   lea rdx,[rcx+$00000654]
000000000059C55C 830202           add dword ptr [rdx],$02
Unit50.pas.40: Inc(FC, 3);
000000000059C55F 488D9158060000   lea rdx,[rcx+$00000658]
000000000059C566 830203           add dword ptr [rdx],$03
Unit50.pas.41: Inc(FD, 4);
000000000059C569 488D915C060000   lea rdx,[rcx+$0000065c]
000000000059C570 830204           add dword ptr [rdx],$04
Unit50.pas.42: end;
000000000059C573 83C001           add eax,$01
000000000059C576 81F840420F00     cmp eax,$000f4240
000000000059C57C 75CD             jnz $000000000059c54b

What is this?, why for every single self access a pointer in rdx is loaded?, is that an issue in the x64 assembler code that I'm not aware?, one cannot do add dword prt [rcx+$000065c], 4?, and what about the increasing plus comparing loop?

Dissapointing?, well, if you are able to modify your code to access self variables as less as possible then the optimization works better, let's see with this slightly modification:

procedure TForm50.FormCreate(Sender: TObject);
var
  a, b, c, d,
  i: integer;

begin
  a := 0;
  b := 0;
  c := 0;
  d := 0;

  for i := 0 to 999999 do
  begin
    Inc(a);
    Inc(b, 2);
    Inc(c, 3);
    Inc(d, 4);
  end;

  FA := a;
  FB := b;
  FC := c;
  FD := d;
end;

and now the assembler code shows:

Unit50.pas.32: a := 0;
000000000059C520 4833C0           xor rax,rax
Unit50.pas.33: b := 0;
000000000059C523 4833D2           xor rdx,rdx
Unit50.pas.34: c := 0;
000000000059C526 4D33C0           xor r8,r8
Unit50.pas.35: d := 0;
000000000059C529 4D33C9           xor r9,r9
Unit50.pas.37: for i := 0 to 999999 do
000000000059C52C 4D33D2           xor r10,r10
Unit50.pas.39: Inc(a);
000000000059C52F 83C001           add eax,$01
Unit50.pas.40: Inc(b, 2);
000000000059C532 83C202           add edx,$02
Unit50.pas.41: Inc(c, 3);
000000000059C535 4183C003         add r8d,$03
Unit50.pas.42: Inc(d, 4);
000000000059C539 4183C104         add r9d,$04
Unit50.pas.43: end;
000000000059C53D 4183C201         add r10d,$01
000000000059C541 4181FA40420F00   cmp r10d,$000f4240
000000000059C548 75E5             jnz $000000000059c52f
000000000059C54A 90               nop
Unit50.pas.45: FA := a;
000000000059C54B 898150060000     mov [rcx+$00000650],eax
Unit50.pas.46: FB := b;
000000000059C551 899154060000     mov [rcx+$00000654],edx
Unit50.pas.47: FC := c;
000000000059C557 44898158060000   mov [rcx+$00000658],r8d
Unit50.pas.48: FD := d;
000000000059C55E 4489895C060000   mov [rcx+$0000065c],r9d

The code looks much better now although the loop still funny (add + cmp). The thing is that in order to speed up ChessKISS in the 64 bits version, I've to remove as much as possible the use of class fields, which is not an easy task, let's see if I got the energy to do so and it yields the expected results.

Wednesday, March 2, 2011

Testing performance

I'm currently in the phase of testing how good compared with old versions is ChessKISS before releasing the 1.0, last night I've setup a tournament:

RankEngineScoreMiChPrPuBiRoChPiMsS-B
1Micromax48_c2d13,0/16· ·0=1=110111111111 86,50 
2ChessKISS12,0/161=· ·=110==11==1111 84,50 
3Predateur 2.010,0/160==0· ·=011011=1111 61,75 
4Pulsar2009-9b8,5/160001=1· ·11=0=10011 60,75 
5BigLion8,5/1610==0000· ·10111=11 54,75 
6Roce388,5/16000010=101· ·==1111 47,25 
7ChessKISS 0.67,0/1600==0==000==· ·1111 38,75 
8Piranha3,5/16000000110=0000· ·01 22,25 
9Mscp1,0/160000000000000010· · 3,50 


Not bad, I still have the problem of the three fold repetition (in most of the draws ChessKISS was clearly in adventage)

So it seems the engine has been improved..., let's see

Monday, October 11, 2010

Speed improvements on TinyChess

A week ago or so I post this log, searching to depth 10 from inital position:

11:20:31, Send: 1 57 0 43 Ngf3 
11:20:31, Send: 2 0 0 227 Ngf3 Ngf6
11:20:31, Send: 3 57 0 503 Ngf3 Ngf6 Nbc3
11:20:31, Send: 4 0 31 2208 Ngf3 Ngf6 Nbc3 Nbc6
11:20:31, Send: 5 27 31 4100 Ngf3 Ngf6 Nbc3 Nbc6 e3
11:20:31, Send: 6 13 141 12055 Nbc3 Nbc6 d4 Ngf6 e4 e6
11:20:31, Send: 7 29 500 39259 Nbc3 Nbc6 Ngf3 Ngf6 e3 d5 Bfd3
11:20:33, Send: 8 10 1734 137817 Nbc3 Ngf6 Ngf3 Nbc6 e3 d5 Bfb5 Bcd7
11:20:36, Send: 9 24 2438 246414 Nbc3 Ngf6 Ngf3 Nbc6 d4 d5 Bcf4 Bcf5 e3
11:20:45, Send: 10 31 9594 624118 Nbc3 Nbc6 Ngf3 e6 e4 Bfb4 Bfd3 Bbxc3 dxc3 Ngf6 



As you can see it took 14 seconds, well this weekend I had to lunch a bunch of processes at work, so meanwhile I had time to deal with speed, compare to this:



10:09:19, Send: 1 55 0 43 Ng1f3 
10:09:19, Send: 2 0 0 223 Ng1f3 Ng8f6 
10:09:19, Send: 3 55 0 473 Ng1f3 Ng8f6 Nb1c3 
10:09:19, Send: 4 0 15 2036 Ng1f3 Ng8f6 Nb1c3 Nb8c6 
10:09:19, Send: 5 20 0 3885 Ng1f3 Ng8f6 Nb1c3 Nb8c6 e3 
10:09:19, Send: 6 0 141 21194 Ng1f3 Nb8c6 Nb1c3 Ng8f6 e3 e5 
10:09:19, Send: 7 25 94 38454 Ng1f3 Nb8c6 Nb1c3 Ng8f6 e3 e5 Bf1d3 
10:09:20, Send: 8 0 609 123812 Ng1f3 Ng8f6 d4 d5 Nb1c3 Nb8c6 Bc1e3 Bc8f5 
10:09:21, Send: 9 17 1125 258263 Ng1f3 Ng8f6 d4 d5 Nb1c3 Nb8c6 e3 Nf6e4 Nc3b5 
10:09:23, Send: 10 15 2500 534927 Ng1f3 Ng8f6 d4 Nb8c6 Nb1c3 d5 e3 Nf6e4 Nc3xe4 d5xe4 


Now the same depth is achieved with only 4,5 seconds (the movements are different since many changes were done), this is 219.293 nodes per second. I'm very satisfied with the results, but this is not finish, the new GenerateAllMoves() totally based on bitboards is on the way, I expect a big performance increase. These were the changes:

  • Bitboards are generated gradually
  • Pieces are generated gradually
  • Evaluation was refactored