Quelle partie du lancement d'une exception coûte cher?


256

En Java, utiliser throw / catch comme une partie de la logique quand il n'y a pas réellement d'erreur est généralement une mauvaise idée (en partie) car lancer et intercepter une exception coûte cher, et le faire plusieurs fois dans une boucle est généralement beaucoup plus lent que les autres structures de contrôle qui n'impliquent pas de levée d'exceptions.

Ma question est, est-ce que le coût est encouru dans le lancer / intercepter lui-même, ou lors de la création de l'objet Exception (car il obtient beaucoup d'informations d'exécution, y compris la pile d'exécution)?

En d'autres termes, si je le fais

Exception e = new Exception();

mais ne le lancez pas, est-ce que c'est la majeure partie du coût du lancer, ou est-ce que le lancer + la prise en charge est ce qui coûte cher?

Je ne demande pas si mettre du code dans un bloc try / catch augmente le coût d'exécution de ce code, je demande si la capture de l'exception est la partie coûteuse, ou la création (appelant le constructeur pour) l'exception est la partie coûteuse .

Une autre façon de le demander est que si je crée une instance d'Exception et que je la lance et la rattrape encore et encore, est-ce que ce sera beaucoup plus rapide que de créer une nouvelle Exception à chaque fois que je lance?


20
Je crois qu'il remplit et remplit la trace de la pile.
Elliott Frisch


"Si j'ai créé une instance d'Exception et que je l'ai lancée et interceptée à plusieurs reprises", lorsque l'exception est créée, sa trace de pile est remplie, ce qui signifie qu'elle sera toujours la même trace de stact indépendamment de l'endroit d'où elle a été lancée. Si stacktrace n'est pas important pour vous, vous pouvez essayer votre idée, mais cela pourrait rendre le débogage très difficile, voire impossible dans certains cas.
Pshemo

2
@Pshemo Je ne prévois pas réellement faire dans le code, je demande sur la performance, et en utilisant cette absurdité comme un exemple où il pourrait faire une différence.
Martin Carney

@MartinCarney J'ai ajouté une réponse à votre dernier paragraphe, c'est-à-dire que la mise en cache d'une exception aurait un gain de performances. Si c'est utile je peux ajouter le code, sinon je peux supprimer la réponse.
Harry

Réponses:


267

La création d' un objet d'exception n'est pas plus coûteuse que la création d'autres objets normaux. Le coût principal est caché dans la fillInStackTraceméthode native qui parcourt la pile d'appels et recueille toutes les informations nécessaires pour construire une trace de pile: classes, noms de méthode, numéros de ligne, etc.

Le mythe des coûts d'exception élevés vient du fait que la plupart des Throwableconstructeurs appellent implicitement fillInStackTrace. Cependant, il existe un constructeur pour créer un Throwablesans trace de pile. Il vous permet de créer des objets jetables très rapides à instancier. Une autre façon de créer des exceptions légères consiste à remplacer fillInStackTrace.


Qu'en est-il maintenant de lever une exception?
En fait, cela dépend de l'endroit où une exception levée est interceptée .

S'il est pris dans la même méthode (ou, plus précisément, dans le même contexte, car le contexte peut inclure plusieurs méthodes en raison de l'incrustation), il throwest aussi rapide et simple que goto(bien sûr, après la compilation JIT).

Cependant, si un catchbloc se trouve quelque part plus profondément dans la pile, la JVM doit alors dérouler les trames de la pile, ce qui peut prendre beaucoup plus de temps. Cela prend encore plus de temps, s'il y a des synchronizedblocs ou des méthodes impliqués, car le déroulement implique la libération des moniteurs appartenant aux trames de pile supprimées.


Je pourrais confirmer les déclarations ci-dessus par des références appropriées, mais heureusement, je n'ai pas besoin de le faire, car tous les aspects sont déjà parfaitement couverts dans le poste de l'ingénieur de la performance de HotSpot Alexey Shipilev: The Exceptional Performance of Lil 'Exception .


8
Comme indiqué dans l'article et abordé ici, le résultat est que le coût des exceptions de lancement / capture dépend fortement de la profondeur des appels. Le fait est que l'énoncé «les exceptions coûtent cher» n'est pas vraiment correct. Une déclaration plus correcte est que les exceptions «peuvent» coûter cher. Honnêtement, je pense que le fait de n'utiliser des exceptions que pour des "cas vraiment exceptionnels" (comme dans l'article) est trop formulé. Ils sont parfaits pour à peu près n'importe quoi en dehors du flux de retour normal et il est difficile de détecter l'impact sur les performances de leur utilisation de cette façon dans une application réelle.
JimmyJames

14
Il pourrait être utile de quantifier les frais généraux des exceptions. Même dans le pire des cas rapporté dans cet article assez exhaustif (lancer et intercepter une exception dynamique avec une trace de pile qui est effectivement interrogée, 1000 images de pile de profondeur), cela prend 80 micro secondes. Cela peut être important si votre système doit traiter des milliers d'exceptions par seconde, mais cela ne vaut pas la peine de s'inquiéter. Et c'est le pire des cas; si vos traces de pile sont un peu plus saines, ou si vous ne les interrogez pas, nous pouvons traiter près d'un million d'exceptions par seconde.
meriton

13
J'insiste sur ce point car de nombreuses personnes, en lisant que les exceptions sont "chères", ne cessent de demander "chères par rapport à quoi", mais supposent qu'elles font "partie chère de leur programme", ce qu'elles sont très rarement.
meriton

2
Il y a une partie qui n'est pas mentionnée ici: le coût potentiel pour empêcher l'application des optimisations. Un exemple extrême serait que la JVM ne soit pas en ligne pour éviter les traces de pile "confuses", mais j'ai vu des (micro) benchmarks où la présence ou l'absence d'exceptions ferait ou casserait des optimisations en C ++ auparavant.
Matthieu M.

3
@MatthieuM. Les exceptions et les blocs try / catch n'empêchent pas la JVM de s'aligner. Pour les méthodes compilées, les traces de pile réelles sont reconstruites à partir de la table de trame de pile virtuelle stockée en tant que métadonnées. Je ne me souviens pas d'une optimisation JIT incompatible avec try / catch. La structure try / catch elle-même n'ajoute rien au code de la méthode, elle n'existe qu'en tant que table d'exceptions en dehors du code.
Apangin

72

La première opération dans la plupart des Throwableconstructeurs consiste à remplir la trace de la pile, où se trouve la majeure partie des dépenses.

Il existe cependant un constructeur protégé avec un indicateur pour désactiver la trace de la pile. Ce constructeur est également accessible lors de l'extension Exception. Si vous créez un type d'exception personnalisé, vous pouvez éviter la création de trace de pile et obtenir de meilleures performances au détriment de moins d'informations.

Si vous créez une seule exception de n'importe quel type par des moyens normaux, vous pouvez la renvoyer plusieurs fois sans avoir à surcharger la trace de la pile. Cependant, sa trace de pile reflétera où il a été construit, pas où il a été lancé dans un cas particulier.

Les versions actuelles de Java tentent d'optimiser la création de trace de pile. Le code natif est appelé pour remplir la trace de la pile, qui enregistre la trace dans une structure native plus légère. Java correspondants StackTraceElementobjets sont paresseusement créés à partir de ce disque que lorsque les getStackTrace(), printStackTrace()ou d' autres méthodes qui nécessitent la trace sont appelés.

Si vous éliminez la génération de trace de pile, l'autre coût principal consiste à dérouler la pile entre le lancer et le crochet. Moins il y aura de trames intermédiaires rencontrées avant que l'exception ne soit interceptée, plus ce sera rapide.

Concevez votre programme de sorte que les exceptions ne soient levées que dans des cas vraiment exceptionnels, et que les optimisations comme celles-ci soient difficiles à justifier.



25

Theres un bon article sur les exceptions ici.

http://shipilev.net/blog/2014/exceptional-performance/

La conclusion étant que la construction de traces de pile et le déroulement de la pile sont les pièces coûteuses. Le code ci-dessous tire parti d'une fonctionnalité 1.7permettant d'activer et de désactiver les traces de pile. Nous pouvons ensuite l'utiliser pour voir quel type de coûts différents scénarios ont

Les temporisations suivantes concernent uniquement la création d'objets. J'ai ajouté Stringici pour que vous puissiez voir que sans l'écriture de la pile, il n'y a presque aucune différence dans la création d'un JavaExceptionobjet et d'un String. Avec l'écriture de pile activée, la différence est dramatique, c'est-à-dire au moins un ordre de grandeur plus lent.

Time to create million String objects: 41.41 (ms)
Time to create million JavaException objects with    stack: 608.89 (ms)
Time to create million JavaException objects without stack: 43.50 (ms)

Ce qui suit montre combien de temps il a fallu pour revenir d'un lancer à une profondeur particulière un million de fois.

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|           1428|             243| 588 (%)|
|   15|           1763|             393| 449 (%)|
|   14|           1746|             390| 448 (%)|
|   13|           1703|             384| 443 (%)|
|   12|           1697|             391| 434 (%)|
|   11|           1707|             410| 416 (%)|
|   10|           1226|             197| 622 (%)|
|    9|           1242|             206| 603 (%)|
|    8|           1251|             207| 604 (%)|
|    7|           1213|             208| 583 (%)|
|    6|           1164|             206| 565 (%)|
|    5|           1134|             205| 553 (%)|
|    4|           1106|             203| 545 (%)|
|    3|           1043|             192| 543 (%)| 

Ce qui suit est presque certainement une simplification excessive ...

Si nous prenons une profondeur de 16 avec l'écriture de la pile, la création d'objet prend environ 40% du temps, la trace de la pile réelle en représente la grande majorité. ~ 93% de l'instanciation de l'objet JavaException est due à la trace de la pile prise. Cela signifie que le déroulement de la pile dans ce cas prend les 50% restants.

Lorsque nous désactivons la création d'objets de trace de pile, elle représente une fraction beaucoup plus petite, c'est-à-dire 20% et le déroulement de la pile représente désormais 80% du temps.

Dans les deux cas, le déroulement de la pile prend une grande partie du temps global.

public class JavaException extends Exception {
  JavaException(String reason, int mode) {
    super(reason, null, false, false);
  }
  JavaException(String reason) {
    super(reason);
  }

  public static void main(String[] args) {
    int iterations = 1000000;
    long create_time_with    = 0;
    long create_time_without = 0;
    long create_string = 0;
    for (int i = 0; i < iterations; i++) {
      long start = System.nanoTime();
      JavaException jex = new JavaException("testing");
      long stop  =  System.nanoTime();
      create_time_with += stop - start;

      start = System.nanoTime();
      JavaException jex2 = new JavaException("testing", 1);
      stop = System.nanoTime();
      create_time_without += stop - start;

      start = System.nanoTime();
      String str = new String("testing");
      stop = System.nanoTime();
      create_string += stop - start;

    }
    double interval_with    = ((double)create_time_with)/1000000;
    double interval_without = ((double)create_time_without)/1000000;
    double interval_string  = ((double)create_string)/1000000;

    System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string);
    System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)\n", iterations, interval_with);
    System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without);

    JavaException jex = new JavaException("testing");
    int depth = 14;
    int i = depth;
    double[] with_stack    = new double[20];
    double[] without_stack = new double[20];

    for(; i > 0 ; --i) {
      without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000;
      with_stack[i]    = jex.timerLoop(i, iterations, 1)/1000000;
    }
    i = depth;
    System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n");
    for(; i > 0 ; --i) {
      double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
      System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio);
      //System.out.printf("%d\t%.2f (ms)\n", i, ratio);
    }
  }
 private int thrower(int i, int mode) throws JavaException {
    ExArg.time_start[i] = System.nanoTime();
    if(mode == 0) { throw new JavaException("without stack", 1); }
    throw new JavaException("with stack");
  }
  private int catcher1(int i, int mode) throws JavaException{
    return this.stack_of_calls(i, mode);
  }
  private long timerLoop(int depth, int iterations, int mode) {
    for (int i = 0; i < iterations; i++) {
      try {
        this.catcher1(depth, mode);
      } catch (JavaException e) {
        ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]);
      }
    }
    //long stop = System.nanoTime();
    return ExArg.time_accum[depth];
  }

  private int bad_method14(int i, int mode) throws JavaException  {
    if(i > 0) { this.thrower(i, mode); }
    return i;
  }
  private int bad_method13(int i, int mode) throws JavaException  {
    if(i == 13) { this.thrower(i, mode); }
    return bad_method14(i,mode);
  }
  private int bad_method12(int i, int mode) throws JavaException{
    if(i == 12) { this.thrower(i, mode); }
    return bad_method13(i,mode);
  }
  private int bad_method11(int i, int mode) throws JavaException{
    if(i == 11) { this.thrower(i, mode); }
    return bad_method12(i,mode);
  }
  private int bad_method10(int i, int mode) throws JavaException{
    if(i == 10) { this.thrower(i, mode); }
    return bad_method11(i,mode);
  }
  private int bad_method9(int i, int mode) throws JavaException{
    if(i == 9) { this.thrower(i, mode); }
    return bad_method10(i,mode);
  }
  private int bad_method8(int i, int mode) throws JavaException{
    if(i == 8) { this.thrower(i, mode); }
    return bad_method9(i,mode);
  }
  private int bad_method7(int i, int mode) throws JavaException{
    if(i == 7) { this.thrower(i, mode); }
    return bad_method8(i,mode);
  }
  private int bad_method6(int i, int mode) throws JavaException{
    if(i == 6) { this.thrower(i, mode); }
    return bad_method7(i,mode);
  }
  private int bad_method5(int i, int mode) throws JavaException{
    if(i == 5) { this.thrower(i, mode); }
    return bad_method6(i,mode);
  }
  private int bad_method4(int i, int mode) throws JavaException{
    if(i == 4) { this.thrower(i, mode); }
    return bad_method5(i,mode);
  }
  protected int bad_method3(int i, int mode) throws JavaException{
    if(i == 3) { this.thrower(i, mode); }
    return bad_method4(i,mode);
  }
  private int bad_method2(int i, int mode) throws JavaException{
    if(i == 2) { this.thrower(i, mode); }
    return bad_method3(i,mode);
  }
  private int bad_method1(int i, int mode) throws JavaException{
    if(i == 1) { this.thrower(i, mode); }
    return bad_method2(i,mode);
  }
  private int stack_of_calls(int i, int mode) throws JavaException{
    if(i == 0) { this.thrower(i, mode); }
    return bad_method1(i,mode);
  }
}

class ExArg {
  public static long[] time_start;
  public static long[] time_accum;
  static {
     time_start = new long[20];
     time_accum = new long[20];
  };
}

Les cadres de pile dans cet exemple sont minuscules par rapport à ce que vous trouverez normalement.

Vous pouvez jeter un œil au bytecode en utilisant javap

javap -c -v -constants JavaException.class

c'est à dire pour la méthode 4 ...

   protected int bad_method3(int, int) throws JavaException;
flags: ACC_PROTECTED
Code:
  stack=3, locals=3, args_size=3
     0: iload_1       
     1: iconst_3      
     2: if_icmpne     12
     5: aload_0       
     6: iload_1       
     7: iload_2       
     8: invokespecial #6                  // Method thrower:(II)I
    11: pop           
    12: aload_0       
    13: iload_1       
    14: iload_2       
    15: invokespecial #17                 // Method bad_method4:(II)I
    18: ireturn       
  LineNumberTable:
    line 63: 0
    line 64: 12
  StackMapTable: number_of_entries = 1
       frame_type = 12 /* same */

Exceptions:
  throws JavaException

13

La création du Exceptionavec une nulltrace de pile prend autant de temps que le throwet le try-catchbloc ensemble. Cependant, le remplissage de la trace de pile prend en moyenne 5 fois plus de temps .

J'ai créé le benchmark suivant pour démontrer l'impact sur les performances. J'ai ajouté le -Djava.compiler=NONEà la configuration d'exécution pour désactiver l'optimisation du compilateur. Pour mesurer l'impact de la création de la trace de pile, j'ai étendu la Exceptionclasse pour tirer parti du constructeur sans pile:

class NoStackException extends Exception{
    public NoStackException() {
        super("",null,false,false);
    }
}

Le code de référence est le suivant:

public class ExceptionBenchmark {

    private static final int NUM_TRIES = 100000;

    public static void main(String[] args) {

        long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0;

        for (int i = 0; i < 30; i++) {
            throwCatchTime += throwCatchLoop();
            newExceptionTime += newExceptionLoop();
            newObjectTime += newObjectLoop();
            noStackExceptionTime += newNoStackExceptionLoop();
        }

        System.out.println("throwCatchTime = " + throwCatchTime / 30);
        System.out.println("newExceptionTime = " + newExceptionTime / 30);
        System.out.println("newStringTime = " + newObjectTime / 30);
        System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30);

    }

    private static long throwCatchLoop() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {

                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newObjectLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new Object();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newNoStackExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            NoStackException e = new NoStackException();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

}

Production:

throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15

Cela implique que la création d'un NoStackExceptionest à peu près aussi coûteuse que son lancement répété Exception. Il montre également que la création Exceptionet le remplissage de sa trace de pile prennent environ 4 fois plus de temps.


1
Pourriez-vous ajouter un cas de plus où vous créez une instance d'exception avant l'heure de début, puis lancez + attrapez-la à plusieurs reprises dans une boucle? Cela montrerait le coût de simplement lancer + attraper.
Martin Carney

@MartinCarney Grande suggestion! J'ai mis à jour ma réponse pour faire exactement cela.
Austin D

J'ai fait quelques ajustements de votre code de test, et il semble que le compilateur fasse une optimisation qui nous empêche d'obtenir des chiffres précis.
Martin Carney

@MartinCarney J'ai mis à jour la réponse à l'optimisation du compilateur d'actualisation
Austin D

Pour info, vous devriez probablement lire les réponses à Comment écrire un micro-benchmark correct en Java? Astuce: ce n'est pas ça.
Daniel Pryden

4

Cette partie de la question ...

Une autre façon de le demander est que si je crée une instance d'Exception et que je la lance et la rattrape encore et encore, est-ce que ce sera beaucoup plus rapide que de créer une nouvelle Exception à chaque fois que je lance?

Semble demander si la création d'une exception et sa mise en cache quelque part améliorent les performances. Oui. C'est la même chose que de désactiver la pile en cours d'écriture lors de la création de l'objet car c'est déjà fait.

Voici les horaires que j'ai reçus, veuillez lire la mise en garde après cela ...

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|            193|             251| 77 (%)| 
|   15|            390|             406| 96 (%)| 
|   14|            394|             401| 98 (%)| 
|   13|            381|             385| 99 (%)| 
|   12|            387|             370| 105 (%)| 
|   11|            368|             376| 98 (%)| 
|   10|            188|             192| 98 (%)| 
|    9|            193|             195| 99 (%)| 
|    8|            200|             188| 106 (%)| 
|    7|            187|             184| 102 (%)| 
|    6|            196|             200| 98 (%)| 
|    5|            197|             193| 102 (%)| 
|    4|            198|             190| 104 (%)| 
|    3|            193|             183| 105 (%)| 

Bien sûr, le problème avec cela est que votre trace de pile indique maintenant où vous avez instancié l'objet et non d'où il a été jeté.


3

En utilisant la réponse de @ AustinD comme point de départ, j'ai fait quelques ajustements. Code en bas.

En plus d'ajouter le cas où une instance d'exception est levée à plusieurs reprises, j'ai également désactivé l'optimisation du compilateur afin que nous puissions obtenir des résultats de performances précis. J'ai ajouté -Djava.compiler=NONEaux arguments VM, selon cette réponse . (Dans eclipse, modifiez la configuration d'exécution → arguments pour définir cet argument VM)

Les resultats:

new Exception + throw/catch = 643.5
new Exception only          = 510.7
throw/catch only            = 115.2
new String (benchmark)      = 669.8

Ainsi, la création de l'exception coûte environ 5 fois plus que le lancer + l'attraper. En supposant que le compilateur n'optimise pas une grande partie du coût.

À titre de comparaison, voici le même test sans désactiver l'optimisation:

new Exception + throw/catch = 382.6
new Exception only          = 379.5
throw/catch only            = 0.3
new String (benchmark)      = 15.6

Code:

public class ExceptionPerformanceTest {

    private static final int NUM_TRIES = 1000000;

    public static void main(String[] args) {

        double numIterations = 10;

        long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0;

        for (int i = 0; i < numIterations; i++) {
            exceptionPlusCatchTime += exceptionPlusCatchBlock();
            excepTime += createException();
            throwTime += catchBlock();
            strTime += createString();
        }

        System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations);
        System.out.println("new Exception only          = " + excepTime / numIterations);
        System.out.println("throw/catch only            = " + throwTime / numIterations);
        System.out.println("new String (benchmark)      = " + strTime / numIterations);

    }

    private static long exceptionPlusCatchBlock() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw new Exception();
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createException() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createString() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new String("" + i);
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long catchBlock() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }
}

Désactiver l'optimisation = une excellente technique! Je vais modifier ma réponse originale afin de ne tromper personne
Austin D

3
Désactiver l'optimisation n'est pas mieux que d'écrire un benchmark imparfait, car le mode purement interprété n'a rien à voir avec les performances du monde réel. La puissance de JVM est le compilateur JIT, alors quel est l'intérêt de mesurer quelque chose qui ne reflète pas le fonctionnement d'une application réelle?
Apangin

2
Il y a beaucoup plus d'aspects de la création, du lancement et de la capture d'exceptions que ce qui a été convergé dans ce «repère». Je vous suggère fortement de lire ce post .
Apangin
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.