Wie misst man die Programmausführungszeit im ARM Cortex-A8-Prozessor?

Lesezeit: 11 Minuten

Benutzer-Avatar
Haggar TheHorrible

Ich verwende einen ARM Cortex-A8-basierten Prozessor namens i.MX515. Es gibt eine Linux Ubuntu 9.10-Distribution. Ich führe eine sehr große Anwendung aus, die in C geschrieben ist und von der ich Gebrauch mache gettimeofday(); Funktionen, um die Dauer meiner Bewerbung zu messen.

main()

{

gettimeofday(start);
....
....
....
gettimeofday(end);

}

Diese Methode war ausreichend, um zu sehen, welche Blöcke meiner Anwendung wie viel Zeit in Anspruch nahmen. Aber jetzt, wo ich versuche, meinen Code sehr gründlich zu optimieren, sehe ich mit der Methode gettimeofday() zur Berechnung der Zeit eine Menge Schwankungen zwischen aufeinanderfolgenden Läufen (Run vor und nach meinen Optimierungen), also bin ich nicht in der Lage um die tatsächlichen Ausführungszeiten zu bestimmen, daher die Auswirkungen meiner Verbesserungen.

Kann mir jemand vorschlagen, was ich tun soll?

Wenn durch Zugriff auf den Zykluszähler (Auf der ARM-Website vorgeschlagene Idee für Cortex-M3) kann mir jemand einen Code zeigen, der mir die Schritte gibt, die ich befolgen muss, um auf den Timer zuzugreifen registriert sich auf Cortex-A8?

Wenn diese Methode nicht sehr genau ist, schlagen Sie bitte einige Alternativen vor.

Vielen Dank


Nachverfolgungen

Follow-up 1: Ich habe das folgende Programm auf Code Sorcery geschrieben, die ausführbare Datei wurde generiert und als ich versuchte, auf dem Board zu laufen, bekam ich – Illegal Instruction Message 🙁

static inline unsigned int get_cyclecount (void)
{
    unsigned int value;
    // Read CCNT Register
    asm volatile ("MRC p15, 0, %0, c9, c13, 0\t\n": "=r"(value));
    return value;
}

static inline void init_perfcounters (int32_t do_reset, int32_t enable_divider)
{
    // in general enable all counters (including cycle counter)
    int32_t value = 1;

    // peform reset:
    if (do_reset)
    {
    value |= 2;     // reset all counters to zero.
    value |= 4;     // reset cycle counter to zero.
    }

    if (enable_divider)
    value |= 8;     // enable "by 64" divider for CCNT.

    value |= 16;

    // program the performance-counter control-register:
    asm volatile ("MCR p15, 0, %0, c9, c12, 0\t\n" :: "r"(value));

    // enable all counters:
    asm volatile ("MCR p15, 0, %0, c9, c12, 1\t\n" :: "r"(0x8000000f));

    // clear overflows:
    asm volatile ("MCR p15, 0, %0, c9, c12, 3\t\n" :: "r"(0x8000000f));
}



int main()
{

    /* enable user-mode access to the performance counter*/
asm ("MCR p15, 0, %0, C9, C14, 0\n\t" :: "r"(1));

/* disable counter overflow interrupts (just in case)*/
asm ("MCR p15, 0, %0, C9, C14, 2\n\t" :: "r"(0x8000000f));

    init_perfcounters (1, 0);

    // measure the counting overhead:
    unsigned int overhead = get_cyclecount();
    overhead = get_cyclecount() - overhead;

    unsigned int t = get_cyclecount();

    // do some stuff here..
    printf("\nHello World!!");

    t = get_cyclecount() - t;

    printf ("function took exactly %d cycles (including function call) ", t - overhead);

    get_cyclecount();

    return 0;
}

Follow-up 2: Ich hatte an Freescale geschrieben, um Unterstützung zu erhalten, und sie haben mir die folgende Antwort und ein Programm zurückgeschickt (Ich habe nicht viel davon verstanden)

Hier ist, womit wir Ihnen jetzt helfen können: Ich schicke Ihnen ein Codebeispiel, das einen Stream mit dem UART sendet, aus Ihrem Code scheint es, dass Sie die MPU nicht richtig initieren.

(hash)include <stdio.h>
(hash)include <stdlib.h>

(hash)define BIT13 0x02000

(hash)define R32   volatile unsigned long *
(hash)define R16   volatile unsigned short *
(hash)define R8   volatile unsigned char *

(hash)define reg32_UART1_USR1     (*(R32)(0x73FBC094))
(hash)define reg32_UART1_UTXD     (*(R32)(0x73FBC040))

(hash)define reg16_WMCR         (*(R16)(0x73F98008))
(hash)define reg16_WSR              (*(R16)(0x73F98002))

(hash)define AIPS_TZ1_BASE_ADDR             0x70000000
(hash)define IOMUXC_BASE_ADDR               AIPS_TZ1_BASE_ADDR+0x03FA8000

typedef unsigned long  U32;
typedef unsigned short U16;
typedef unsigned char  U8;


void serv_WDOG()
{
    reg16_WSR = 0x5555;
    reg16_WSR = 0xAAAA;
}


void outbyte(char ch)
{
    while( !(reg32_UART1_USR1 & BIT13)  );

    reg32_UART1_UTXD = ch ;
}


void _init()
{

}



void pause(int time) 
{
    int i;

    for ( i=0 ; i < time ;  i++);

} 


void led()
{

//Write to Data register [DR]

    *(R32)(0x73F88000) = 0x00000040;  // 1 --> GPIO 2_6 
    pause(500000);

    *(R32)(0x73F88000) = 0x00000000;  // 0 --> GPIO 2_6 
    pause(500000);


}

void init_port_for_led()
{


//GPIO 2_6   [73F8_8000] EIM_D22  (AC11)    DIAG_LED_GPIO
//ALT1 mode
//IOMUXC_SW_MUX_CTL_PAD_EIM_D22  [+0x0074]
//MUX_MODE [2:0]  = 001: Select mux mode: ALT1 mux port: GPIO[6] of instance: gpio2.

 // IOMUXC control for GPIO2_6

*(R32)(IOMUXC_BASE_ADDR + 0x74) = 0x00000001; 

//Write to DIR register [DIR]

*(R32)(0x73F88004) = 0x00000040;  // 1 : GPIO 2_6  - output

*(R32)(0x83FDA090) = 0x00003001;
*(R32)(0x83FDA090) = 0x00000007;


}

int main ()
{
  int k = 0x12345678 ;

    reg16_WMCR = 0 ;                        // disable watchdog
    init_port_for_led() ;

    while(1)
    {
        printf("Hello word %x\n\r", k ) ;
        serv_WDOG() ;
        led() ;

    }

    return(1) ;
}

  • Sie können immer die mittlere Ausführungszeit über eine große Anzahl von Durchläufen nehmen.

    – Café

    15. Juli 2010 um 0:07 Uhr

  • Beispielcode der Performance Monitor Unit für ARM11 und Cortex-A/R: infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/…

    – 0x90

    5. September 2013 um 15:05 Uhr


Benutzer-Avatar
Nils Pipenbrink

Der Zugriff auf die Leistungsindikatoren ist nicht schwierig, aber Sie müssen sie im Kernel-Modus aktivieren. Standardmäßig sind die Zähler deaktiviert.

Kurz gesagt müssen Sie die folgenden zwei Zeilen innerhalb des Kernels ausführen. Entweder als ladbares Modul oder einfach das Hinzufügen der beiden Zeilen irgendwo in der Board-Init reicht aus:

  /* enable user-mode access to the performance counter*/
  asm ("MCR p15, 0, %0, C9, C14, 0\n\t" :: "r"(1));

  /* disable counter overflow interrupts (just in case)*/
  asm ("MCR p15, 0, %0, C9, C14, 2\n\t" :: "r"(0x8000000f));

Sobald Sie dies getan haben, beginnt der Zykluszähler für jeden Zyklus zu inkrementieren. Überläufe des Registers bleiben unbemerkt und verursachen keine Probleme (außer sie könnten Ihre Messungen durcheinander bringen).

Nun wollen Sie aus dem User-Mode auf den Cycle-Counter zugreifen:

Wir beginnen mit einer Funktion, die das Register liest:

static inline unsigned int get_cyclecount (void)
{
  unsigned int value;
  // Read CCNT Register
  asm volatile ("MRC p15, 0, %0, c9, c13, 0\t\n": "=r"(value));  
  return value;
}

Und Sie möchten höchstwahrscheinlich auch den Teiler zurücksetzen und einstellen:

static inline void init_perfcounters (int32_t do_reset, int32_t enable_divider)
{
  // in general enable all counters (including cycle counter)
  int32_t value = 1;

  // peform reset:  
  if (do_reset)
  {
    value |= 2;     // reset all counters to zero.
    value |= 4;     // reset cycle counter to zero.
  } 

  if (enable_divider)
    value |= 8;     // enable "by 64" divider for CCNT.

  value |= 16;

  // program the performance-counter control-register:
  asm volatile ("MCR p15, 0, %0, c9, c12, 0\t\n" :: "r"(value));  

  // enable all counters:  
  asm volatile ("MCR p15, 0, %0, c9, c12, 1\t\n" :: "r"(0x8000000f));  

  // clear overflows:
  asm volatile ("MCR p15, 0, %0, c9, c12, 3\t\n" :: "r"(0x8000000f));
}

do_reset setzt den Zykluszähler auf Null. Einfach so.

enable_diver aktiviert den 1/64 Zyklusteiler. Ohne dieses gesetzte Flag messen Sie jeden Zyklus. Wenn es aktiviert ist, wird der Zähler alle 64 Zyklen erhöht. Dies ist nützlich, wenn Sie lange Zeiten messen möchten, die sonst zum Überlaufen des Zählers führen würden.

Wie man es benutzt:

  // init counters:
  init_perfcounters (1, 0); 

  // measure the counting overhead:
  unsigned int overhead = get_cyclecount();
  overhead = get_cyclecount() - overhead;    

  unsigned int t = get_cyclecount();

  // do some stuff here..
  call_my_function();

  t = get_cyclecount() - t;

  printf ("function took exactly %d cycles (including function call) ", t - overhead);

Sollte auf allen Cortex-A8-CPUs funktionieren.

Oh – und einige Anmerkungen:

Mit diesen Zählern messen Sie die genaue Zeit zwischen den beiden Anrufen get_cyclecount() einschließlich allem, was in anderen Prozessen oder im Kernel ausgegeben wird. Es gibt keine Möglichkeit, die Messung auf Ihren Prozess oder einen einzelnen Thread zu beschränken.

Ruft auch an get_cyclecount() ist nicht kostenlos. Es wird zu einer einzigen asm-Anweisung kompiliert, aber Bewegungen vom Coprozessor blockieren die gesamte ARM-Pipeline. Der Aufwand ist ziemlich hoch und kann Ihre Messung verfälschen. Glücklicherweise ist auch der Overhead festgelegt, sodass Sie ihn messen und von Ihren Timings abziehen können.

In meinem Beispiel habe ich das für jede Messung gemacht. Machen Sie das in der Praxis nicht. Zwischen den beiden Anrufen wird früher oder später ein Interrupt auftreten und Ihre Messungen noch weiter verzerren. Ich schlage vor, dass Sie den Overhead ein paar Mal auf einem inaktiven System messen, alle Außenstehenden ignorieren und stattdessen eine feste Konstante verwenden.

  • Lieber Nils, nochmals vielen Dank für diese schnelle und ausführliche Antwort. Ich möchte bei diesem Ansatz Schritt für Schritt vorgehen, weil ich lernen möchte, wie das alles funktioniert, also habe ich mit einem sehr einfachen Niveau begonnen. Ich habe noch nie in Assembler programmiert und kenne nicht alle Voraussetzungen, also ertragen Sie bitte meine Unwissenheit. Ich habe eine neue Hauptdatei geschrieben und die ersten beiden Zeilen in main(){} eingefügt und sie mit gcc kompiliert. Ich hatte keine Kompilierungsfehler, eine endgültige ausführbare Datei wurde generiert und beim Ausführen erhalte ich “Illegal Instruction”. Haben Sie hier etwas verpasst?

    – HaggarTheHorrible

    15. Juli 2010 um 7:59 Uhr


  • Nils, ich habe CodeSorcery g++ IDE (30-Tage-Testversion) auf meinem Linux-Desktopsystem installiert. Ich dachte daran, mein Projekt dort zu erstellen (mit Cross-Compiler-Tools) und dann die ausführbare Datei auf meinem i.MX515-Board zu verwenden. Ich habe das Programm so geschrieben, wie Sie es erwähnt haben, die ausführbare Datei wurde generiert. Ich habe versucht, zu debuggen (auf dem Emulator), aber die Code-Zauberei hat einen Fehler als Illegal Instruction ausgegeben und es wurde gestoppt, naja, es war für mich dort sowieso nicht so wichtig. Ich habe die ausführbare Datei auf mein i.MX515-Board kopiert und versucht, sie auszuführen, aber ich habe erneut eine Nachricht mit einer ungültigen Anweisung erhalten: ((Ich habe die Frage bearbeitet)

    – HaggarTheHorrible

    15. Juli 2010 um 12:20 Uhr


  • Ich wusste nichts über die zwei Zustände, in denen ein Betriebssystem funktioniert: Kernel- und Benutzermodus. Ich habe es gerade von meiner Kollegin erfahren. Vielleicht arbeite ich gerade im Benutzermodus, das ist der Grund, warum ich, obwohl mein Programm fehlerfrei kompiliert wird, die Meldung “Illegal Instruction” erhalte.

    – HaggarTheHorrible

    15. Juli 2010 um 14:20 Uhr

  • @vikramtheone, Die ersten beiden Zeilen müssen im Kernelmodus ausgeführt werden. Sie ermöglichen den Zugriff im Benutzermodus auf die CCNT- (und zugehörige) Register. Daran führt kein Weg vorbei. Der einfachste Weg ist meiner Meinung nach, ein superkurzes Kernelmodul zu schreiben, das dies tut. Das Kompilieren dieser Module benötigt die Kernel-Header des Kernels, den Sie auf Ihrem Board ausführen, aber da Sie Ubuntu verwenden, sollte das kein großes Problem sein. Hier ist eine minimale Kernel-Modul-Quelle: torus.untergrund.net/code/perfcnt_enable.c

    – Nils Pipenbrinck

    15. Juli 2010 um 22:47 Uhr

  • Sie kompilieren es (auf dem Ziel!) mit make -C SUBDIRS=$(PWD) modules . Das sollte eine Datei namens perfcnt_enable.ko erzeugen, die Sie (auf dem Ziel) mit insmod ./perfcnt_enable.ko laden können. dmesg wird Ihnen sagen, ob es funktioniert hat oder nicht.

    – Nils Pipenbrinck

    15. Juli 2010 um 22:49 Uhr

Sie müssen Ihren Code vor und nach Ihren Optimierungen mit Leistungsanalysetools profilieren.

Konto ist eine Befehlszeile und eine Funktion, mit der Sie Ihre Ressourcen überwachen können. Sie können mehr über die Verwendung und Anzeige der von acct generierten dat-Datei googeln.

Ich werde diesen Beitrag mit anderen Open-Source-Leistungsanalyse-Tools aktualisieren.

Gprof ist ein weiteres solches Werkzeug. Bitte überprüfen Sie die Dokumentation auf dasselbe.

  • Praveen, das Problem, dem ich zuvor mit Leistungsanalyse-Tools (z. B. gprof) begegnet bin, ist, dass die Statistiken, die ich erhalte, keinen Sinn ergeben, wenn ich die Optimierungs-Flags (-O3) einschalte. Es ist eine Weile her, dass ich gprof aus diesem Grund verwendet habe, ich werde es jetzt versuchen, lassen Sie mich sehen.

    – HaggarTheHorrible

    14. Juli 2010 um 15:06 Uhr


  • @vikramtheone – Angenommen, Sie erstellen eine Kontodatei pro Funktionsaufruf, können Sie detaillierte Informationen zu den verwendeten Ressourcen in Bezug auf Zeit und andere Parameter erhalten. Ich habe dies verwendet, um Codeoptimierungen auf Funktionsebene zu profilieren und zu vergleichen. Alternativ können Sie mit gettimeofday auf die Struktur time_t zugreifen und die Ausführungszeit der Funktion auch auf Mikrosekundenebene abrufen. Es kommt also darauf an, was man damit erreichen möchte.

    – Praveen S

    14. Juli 2010 um 15:17 Uhr

  • Praveen, ich werde mich darum kümmern. Was gettimeofday betrifft, verwende ich es ab sofort, aber das Problem, dem ich gegenüberstehe, sind viele Schwankungen in der Zeit, die es jedes Mal misst, daher denke ich, dass die direkte Zeitmessung eher nicht sehr geeignet ist, stattdessen eine andere zu verwenden Eine Entität, die konstant bleibt, egal wie viele Prozesse laufen, ist nützlicher, und eine solche Entität ist die Zykluszählung. Zumindest denke ich jetzt, dass es konstant bleiben wird, weiß nicht, was die Wahrheit erwartet.

    – HaggarTheHorrible

    15. Juli 2010 um 8:15 Uhr

  • @vikramtheone – Nun, in diesem Fall können Sie den Code profilieren. Sie können über getruusage() nachsehen. Und die Profiling-Tools wie Acct und grof geben Ihnen einen Überblick über das Ausführungszeitszenario. Wenn Sie jedoch klar erklären können, mit welcher Art von Inkonsistenz Sie konfrontiert sind, erhalten Sie bessere Antworten, da die Profilerstellung eine wichtige Aktivität vor der Veröffentlichung ist.

    – Praveen S

    15. Juli 2010 um 9:27 Uhr

Um die Antwort von Nils zu erweitern, nachdem ein paar Jahre vergangen sind! – Eine einfache Möglichkeit, auf diese Zähler zuzugreifen, ist Bauen Sie den Kernel mit gator. Dieser meldet dann Zählerwerte zur Verwendung mit Rationalisierendas Leistungsanalysetool von ARM.

Es zeigt jede Funktion auf einer Zeitleiste an (was Ihnen einen allgemeinen Überblick über die Leistung Ihres Systems gibt) und zeigt Ihnen genau, wie lange die Ausführung gedauert hat, zusammen mit der CPU in %, die sie belegt hat. Sie können dies mit Diagrammen jedes Zählers vergleichen, den Sie zum Sammeln eingerichtet haben, und CPU-intensive Aufgaben bis auf Quellcodeebene verfolgen.

Streamline funktioniert mit allen Prozessoren der Cortex-A-Serie.

Ich habe in einer Toolchain für ARM7 gearbeitet, die einen Simulator auf Befehlsebene hatte. Das Ausführen von Apps darin könnte Timings für einzelne Zeilen und / oder Asm-Anweisungen liefern. Das war großartig für eine Mikrooptimierung einer bestimmten Routine. Dieser Ansatz ist jedoch wahrscheinlich nicht für die Optimierung einer ganzen App/eines ganzen Systems geeignet.

1374410cookie-checkWie misst man die Programmausführungszeit im ARM Cortex-A8-Prozessor?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy