Question

J'ai un énorme fichier à analyser ligne par ligne. La vitesse est essentielle.

Exemple de ligne:

Token-1   Here-is-the-Next-Token      Last-Token-on-Line
      ^                        ^
   Current                 Position
   Position              after GetToken

GetToken est appelé, renvoyant "Here-is-the-Next-Token". et définit CurrentPosition sur la position du dernier caractère du jeton afin qu'il soit prêt pour le prochain appel à GetToken. Les jetons sont séparés par un ou plusieurs espaces.

Supposons que le fichier est déjà dans une liste de chaînes en mémoire. Il tient facilement dans la mémoire, disons 200 Mo.

Je ne m'inquiète que du temps d'exécution de l'analyse. Quel code produira l’exécution la plus rapide absolue en Delphi (Pascal)?

Était-ce utile?

La solution

  • Utilisez PChar incrémentant pour accélérer le traitement
  • Si certains jetons ne sont pas nécessaires, copiez uniquement les données de jeton à la demande
  • Copier PChar dans une variable locale lors de la numérisation de caractères
  • Conservez les données source dans un seul tampon, sauf si vous devez gérer ligne par ligne et même dans ce cas, envisagez de traiter le traitement de la ligne comme un jeton distinct dans le système de reconnaissance de lexer
  • Pensez à traiter un tampon de tableau d'octets provenant directement du fichier, si vous connaissez le codage. si vous utilisez Delphi 2009, utilisez PAnsiChar au lieu de PChar, sauf si vous savez bien que le codage est UTF16-LE.
  • Si vous savez que le seul espace sera le # 32 (espace ASCII) ou un ensemble de caractères similaire, il peut y avoir des manipulations de bits astucieuses qui peuvent vous permettre de traiter 4 octets à la fois à l'aide de l'analyse Integer. . Je ne m'attendrais pas à de grandes victoires ici, cependant, et le code sera aussi clair que de la boue.

Voici un exemple de lexer qui devrait être assez efficace, mais il suppose que toutes les données source sont dans une seule chaîne. Le retravailler pour gérer les tampons est moyennement difficile en raison de la longueur des jetons.

type
  TLexer = class
  private
    FData: string;
    FTokenStart: PChar;
    FCurrPos: PChar;
    function GetCurrentToken: string;
  public
    constructor Create(const AData: string);
    function GetNextToken: Boolean;
    property CurrentToken: string read GetCurrentToken;
  end;

{ TLexer }

constructor TLexer.Create(const AData: string);
begin
  FData := AData;
  FCurrPos := PChar(FData);
end;

function TLexer.GetCurrentToken: string;
begin
  SetString(Result, FTokenStart, FCurrPos - FTokenStart);
end;

function TLexer.GetNextToken: Boolean;
var
  cp: PChar;
begin
  cp := FCurrPos; // copy to local to permit register allocation

  // skip whitespace; this test could be converted to an unsigned int
  // subtraction and compare for only a single branch
  while (cp^ > #0) and (cp^ <= #32) do
    Inc(cp);

  // using null terminater for end of file
  Result := cp^ <> #0;

  if Result then
  begin
    FTokenStart := cp;
    Inc(cp);
    while cp^ > #32 do
      Inc(cp);
  end;

  FCurrPos := cp;
end;

Autres conseils

Voici une implémentation boiteuse d’un lexer très simple. Cela pourrait vous donner une idée.

Notez les limites de cet exemple: pas de mise en mémoire tampon, pas d'Unicode (extrait d'un projet Delphi 7). Vous auriez probablement besoin de ceux-ci dans une mise en œuvre sérieuse.

{ Implements a simpe lexer class. } 
unit Simplelexer;

interface

uses Classes, Sysutils, Types, dialogs;

type

  ESimpleLexerFinished = class(Exception) end;

  TProcTableProc = procedure of object;

  // A very simple lexer that can handle numbers, words, symbols - no comment handling  
  TSimpleLexer = class(TObject)
  private
    FLineNo: Integer;
    Run: Integer;
    fOffset: Integer;
    fRunOffset: Integer; // helper for fOffset
    fTokenPos: Integer;
    pSource: PChar;
    fProcTable: array[#0..#255] of TProcTableProc;
    fUseSimpleStrings: Boolean;
    fIgnoreSpaces: Boolean;
    procedure MakeMethodTables;
    procedure IdentProc;
    procedure NewLineProc;
    procedure NullProc;
    procedure NumberProc;
    procedure SpaceProc;
    procedure SymbolProc;
    procedure UnknownProc;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Feed(const S: string);
    procedure Next;
    function GetToken: string;
    function GetLineNo: Integer;
    function GetOffset: Integer;

    property IgnoreSpaces: boolean read fIgnoreSpaces write fIgnoreSpaces;
    property UseSimpleStrings: boolean read fUseSimpleStrings write fUseSimpleStrings;
  end;

implementation

{ TSimpleLexer }

constructor TSimpleLexer.Create;
begin
  makeMethodTables;
  fUseSimpleStrings := false;
  fIgnoreSpaces := false;
end;

destructor TSimpleLexer.Destroy;
begin
  inherited;
end;

procedure TSimpleLexer.Feed(const S: string);
begin
  Run := 0;
  FLineNo := 1;
  FOffset := 1;
  pSource := PChar(S);
end;

procedure TSimpleLexer.Next;
begin
  fTokenPos := Run;
  foffset := Run - frunOffset + 1;
  fProcTable[pSource[Run]];
end;

function TSimpleLexer.GetToken: string;
begin
  SetString(Result, (pSource + fTokenPos), Run - fTokenPos);
end;

function TSimpleLexer.GetLineNo: Integer;
begin
  Result := FLineNo;
end;

function TSimpleLexer.GetOffset: Integer;
begin
  Result := foffset;
end;

procedure TSimpleLexer.MakeMethodTables;
var
  I: Char;
begin
  for I := #0 to #255 do
    case I of
      '@', '&', '}', '{', ':', ',', ']', '[', '*',
        '^', ')', '(', ';', '/', '=', '-', '+', '#', '>', '<', ',
        '.', '"', #39:
        fProcTable[I] := SymbolProc;
      #13, #10: fProcTable[I] := NewLineProc;
      'A'..'Z', 'a'..'z', '_': fProcTable[I] := IdentProc;
      #0: fProcTable[I] := NullProc;
      '0'..'9': fProcTable[I] := NumberProc;
      #1..#9, #11, #12, #14..#32: fProcTable[I] := SpaceProc;
    else
      fProcTable[I] := UnknownProc;
    end;
end;

procedure TSimpleLexer.UnknownProc;
begin
  inc(run);
end;

procedure TSimpleLexer.SymbolProc;
begin
  if fUseSimpleStrings then
  begin
    if pSource[run] = '"' then
    begin
      Inc(run);
      while pSource[run] <> '"' do
      begin
        Inc(run);
        if pSource[run] = #0 then
        begin
          NullProc;
        end;
      end;
    end;
    Inc(run);
  end
  else
    inc(run);
end;

procedure TSimpleLexer.IdentProc;
begin
  while pSource[Run] in ['_', 'A'..'Z', 'a'..'z', '0'..'9'] do
    Inc(run);
end;

procedure TSimpleLexer.NumberProc;
begin
  while pSource[run] in ['0'..'9'] do
    inc(run);
end;

procedure TSimpleLexer.SpaceProc;
begin
  while pSource[run] in [#1..#9, #11, #12, #14..#32] do
    inc(run);
  if fIgnoreSpaces then Next;
end;

procedure TSimpleLexer.NewLineProc;
begin
  inc(FLineNo);
  inc(run);
  case pSource[run - 1] of
    #13:
      if pSource[run] = #10 then inc(run);
  end;
  foffset := 1;
  fRunOffset := run;
end;

procedure TSimpleLexer.NullProc;
begin
  raise ESimpleLexerFinished.Create('');
end;

end.

J'ai fabriqué un analyseur lexical basé sur un moteur d'état (DFA). Cela fonctionne avec une table et est assez rapide. Mais il existe des options plus rapides possibles.

Cela dépend aussi de la langue. Un langage simple peut éventuellement avoir un algorithme intelligent.

La table est un tableau d’enregistrements contenant chacun 2 caractères et 1 entier. Pour chaque jeton, le lexer parcourt la table en commençant à la position 0:

state := 0;
result := tkNoToken;
while (result = tkNoToken) do begin
  if table[state].c1 > table[state].c2 then
    result := table[state].value
  else if (table[state].c1 <= c) and (c <= table[state].c2) then begin
    c := GetNextChar();
    state := table[state].value;
  end else
    Inc(state);
end;

C’est simple et fonctionne comme un charme.

Si la rapidité est essentielle, le code personnalisé est la réponse. Découvrez l'API Windows qui mappera votre fichier en mémoire. Vous pouvez ensuite simplement utiliser un pointeur sur le caractère suivant pour créer vos jetons, en passant comme vous le souhaitez.

Ceci est mon code pour faire un mapping:

procedure TMyReader.InitialiseMapping(szFilename : string);
var
//  nError : DWORD;
    bGood : boolean;
begin
    bGood := False;
    m_hFile := CreateFile(PChar(szFilename), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
    if m_hFile <> INVALID_HANDLE_VALUE then
    begin
        m_hMap := CreateFileMapping(m_hFile, nil, PAGE_READONLY, 0, 0, nil);
        if m_hMap <> 0 then
        begin
            m_pMemory := MapViewOfFile(m_hMap, FILE_MAP_READ, 0, 0, 0);
            if m_pMemory <> nil then
            begin
                htlArray := Pointer(Integer(m_pMemory) + m_dwDataPosition);
                bGood := True;
            end
            else
            begin
//              nError := GetLastError;
            end;
        end;
    end;
    if not bGood then
        raise Exception.Create('Unable to map token file into memory');
end;

Je pense que le plus gros goulot d’étranglement sera toujours d’obtenir le fichier en mémoire. Une fois que vous l'avez en mémoire (évidemment pas tout à la fois, mais je travaillerais avec des tampons si j'étais vous), l'analyse devrait être insignifiante.

Cela soulève une autre question: quelle taille? Donnez-nous un indice comme # de lignes ou # ou Mb (Go)? Ensuite, nous saurons si cela tient dans la mémoire, s'il doit être basé sur un disque, etc.

Au premier passage, j'utilisais ma liste de mots (S: String; AList: TStringlist);

alors vous pouvez accéder à chaque jeton en tant que Alist [n] ... ou les trier ou autre chose.

La vitesse sera toujours relative à ce que vous faites une fois analysée. Un analyseur lexical est de loin la méthode la plus rapide de conversion en jetons à partir d’un flux de texte, quelle que soit sa taille. TParser dans les classes est un excellent point de départ.

Personnellement, cela fait un moment que je n'ai pas besoin d'écrire un analyseur, mais une autre méthode plus ancienne et éprouvée consiste à utiliser LEX / YACC pour créer une grammaire, puis à la convertir en code que vous pouvez utiliser pour effectuer vos tâches. En traitement. DYacc est une version de Delphi ... ne sachant pas si elle est toujours compilée ou non, mais vaut le coup d'oeil si vous voulez faire les choses vieille école. Le livre de dragons ici serait d'une grande aide si vous pouvez en trouver un exemplaire.

Bien sûr, rouler vous-même est le moyen le plus rapide. Pour plus d'informations à ce sujet, vous pouvez consulter le code source de Synedit qui contient des lexeurs (appelés surligneurs dans le contexte du projet). pour à peu près n'importe quelle langue sur le marché. Je vous suggère de prendre un de ces lexers comme base et de le modifier pour votre propre usage.

Le moyen le plus rapide de écrire le code consisterait probablement à créer une liste TStringList et à affecter chaque ligne de votre fichier texte à la propriété CommaText. Par défaut, l'espace blanc est un délimiteur. Vous obtiendrez ainsi un élément StringList par jeton.

MyStringList.CommaText := s;
for i := 0 to MyStringList.Count - 1 do
begin
  // process each token here
end;

Vous obtiendrez probablement de meilleures performances en analysant chaque ligne vous-même, cependant.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top