1 points par GN⁺ 2023-10-22 | 1 commentaires | Partager sur WhatsApp
  • Le Python steering council a indiqué son intention d’approuver la PEP 703, qui rend le GIL optionnel sur plusieurs versions, mais les conditions finales sont encore en cours d’ajustement
  • Le build --disable-gil de CPython 3.13 sera disponible à titre expérimental, et la compatibilité avec la stable ABI et les wheels de modules d’extension apparaît comme le principal enjeu technique
  • Les wheels abi3 existantes pourraient ne pas être directement compatibles avec CPython 3.13 sans GIL ; l’introduction d’abi4, des modifications de l’API C limitée et la conversion des macros de comptage de références en appels de fonctions sont discutées
  • On craint que pip choisisse la mauvaise wheel entre les builds avec GIL et sans GIL ; une installation silencieusement incorrecte serait plus dangereuse qu’un simple échec d’installation
  • Pour nommer le build sans GIL, free-threading a été proposé à la place de nogil, mais il faut aussi résoudre les questions de nom de l’exécutable, de shebang, d’installation en parallèle et de packaging par les distributions

Position actuelle de la PEP 703 et de CPython sans GIL

  • Le Python steering council a annoncé fin juillet son intention d’approuver la PEP 703, qui vise à rendre le verrou global de l’interpréteur (GIL) optionnel dans CPython
  • Les conditions détaillées d’approbation ne sont pas encore finalisées, mais les discussions d’implémentation et la préparation de l’écosystème sont déjà en cours
  • À long terme, la trajectoire envisagée mène à une version unique de CPython sans GIL, mais, pour l’instant, il s’agit de tester le fonctionnement sans GIL dans un interpréteur compilé avec l’option --disable-gil
  • CPython 3.13 est prévu pour octobre 2024, et le build sans GIL de cette version aura un caractère expérimental

Stable ABI et compatibilité des modules d’extension

  • Sam Gross a abordé sur le forum de discussion Python la façon dont la PEP 703 s’articule avec la stable ABI de CPython
  • La stable ABI sert à permettre aux modules d’extension de fonctionner sur plusieurs versions de CPython avec la même wheel binaire, sans devoir être recompilés à chaque nouvelle version de CPython
  • Les extensions compilées pour la stable ABI pourraient ne pas fonctionner telles quelles avec le build sans GIL de CPython 3.13
  • Pour résoudre cela, plusieurs ajouts et modifications de l’API C limitée sont proposés
    • Les extensions qui n’utilisent que l’API C limitée peuvent produire des binaires utilisant la stable ABI
    • Les changements envisagés incluent le plan déjà prévu de convertir certaines macros qui incrémentent ou décrémentent le compteur de références des objets en appels de fonctions
  • L’objectif est de permettre des binaires d’extension capables de fonctionner à la fois avec les builds avec GIL et sans GIL

abi3, abi4 et le problème du choix des wheels

  • Victor Stinner estime que, pour que l’expérimentation sans GIL réussisse, il faut une solution simple pour les extensions fonctionnant avec les deux types d’interpréteurs
  • Les extensions compilées pour la stable ABI avec CPython 3.12 ou antérieur ne seront pas compatibles avec les builds sans GIL à partir de 3.13 ; l’idée de créer une nouvelle version d’ABI, abi4, a donc émergé
    • La stable ABI actuelle est abi3
    • Le numéro d’ABI et le numéro de version majeure de CPython ne sont pas nécessairement liés
  • Gross considère que les extensions souhaitant prendre en charge le mode sans GIL peuvent, dans une certaine mesure, accepter la charge de produire deux wheels binaires
    • Il s’inquiète davantage d’une situation où le projet sans GIL serait trop fortement contraint par les améliorations de l’API C et de la stable ABI
  • Alex Gaynor maintient lui aussi plusieurs paquets de wheels abi3, mais estime que produire une fois deux wheels n’est pas une charge excessive
    • Toutefois, il est important que les versions actuelles et futures de pip choisissent la bonne wheel parmi les deux
  • Brett Cannon estime que la logique actuelle de pip ne distingue pas les deux versions ; sans changement comme abi4, les versions existantes et anciennes de pip ne fonctionneront donc pas correctement

Risque de mauvais comportement silencieux de pip

  • Gross estime qu’il n’est pas nécessaire de trop s’inquiéter de la prise en charge des anciens pip dans le build expérimental --disable-gil de CPython 3.13
    • La raison avancée est qu’il est courant qu’un ancien pip casse avec une nouvelle version de Python
    • Il cite comme exemple le fait que pip==23.1.1 et les versions antérieures cassent avec CPython 3.13 en raison de l’absence de pkgutil.ImpImporter
  • Paul Moore, mainteneur de pip, considère qu’une rupture explicite et l’installation silencieuse d’un paquet incorrect sont deux problèmes différents
    • Certains utilisateurs emploient encore d’anciennes versions de pip
    • Un échec explicite et une erreur silencieuse n’ont pas le même impact pour l’utilisateur
  • Moore craint que les utilisateurs voulant tester des builds sans GIL ou free-threaded ne soient découragés s’ils doivent déboguer des problèmes de compatibilité ABI
  • Gaynor estime lui aussi que, si pip se comporte silencieusement de manière incorrecte pour les paquets concernés, les tickets pourraient affluer

Installation en parallèle et nom de l’exécutable

  • Barry Warsaw a demandé s’il existait un plan pour installer côte à côte, sur un même système, les builds avec GIL et sans GIL
  • Gross a répondu que cette situation était comparable à l’installation de différentes versions de Python
  • Cannon estime qu’une solution consistant à placer deux binaires dans une seule wheel « fat » serait également possible
    • Toutefois, les noms des binaires à l’intérieur de la wheel devraient être différents
  • La discussion sur le nom de l’exécutable s’est poursuivie dans un fil séparé
  • Paul Moore considère que les utilisateurs doivent pouvoir tester facilement le mode sans GIL et choisir facilement entre GIL et sans GIL
    • Si ce processus est difficile sous Windows, macOS, Linux, etc., cela pourrait avoir un impact négatif sur le projet sans GIL
    • Il faut que les utilisateurs puissent l’essayer facilement pour qu’une demande de builds sans GIL apparaisse, ce qui créera aussi une pression sur les mainteneurs de paquets afin qu’ils fournissent des wheels compatibles sans GIL

Débat sur les noms nogil et free-threading

  • Barry Scott estime que le nom de l’exécutable est important, car la ligne shebang doit indiquer quel interpréteur appeler
    • Il propose par exemple des noms comme python-nogil3 ou python-nogil3.13
  • Gregory P. Smith a exprimé l’avis personnel que, le build sans GIL de CPython 3.13 étant une fonctionnalité expérimentale, les distributions ne devraient pas le placer dans le $PATH par défaut
    • Il considère également qu’il n’est pas souhaitable que des noms d’exécutables longs restent durablement dans les shebangs
    • Il suggère de repousser la décision sur le nom d’installation à la version 3.14 ou ultérieure
  • Petr Viktorin, développeur Fedora, souligne qu’il est probable que les distributions veuillent packager l’interpréteur sans GIL pour permettre aux utilisateurs d’expérimenter
  • Moore indique vouloir pouvoir désigner un build free-threaded sous une forme comme #!/usr/bin/env python3.13-nogil
    • Il s’agit d’éviter de coder en dur un chemin long et peu intuitif
  • Dans un fil sur l’installateur Windows lancé par Steve Dower, Smith a indiqué que le steering council souhaitait éviter le nom nogil
    • Les raisons sont qu’il ne parle pas clairement à la plupart des développeurs non core, qu’il ne devrait pas être nécessaire de savoir ce qu’est le GIL, et qu’il contient une formulation négative
    • Le terme free-threading a été proposé comme alternative
  • Gross estime que free-threading n’est pas non plus facile à comprendre pour les personnes extérieures et que ce n’est pas un terme largement utilisé
  • Dans les discussions concrètes, la préférence allait nettement à un nom court, et nogil était le candidat le plus solide sur ce point
  • Le changement effectivement reflété consiste à remplacer le tag ABI des builds sans GIL de n par t
    • t signifie threading

Proposition abi4 et travaux restants

  • Gross et Viktorin ont discuté des points problématiques de la proposition de modification de l’API, et ces retours ont conduit à la proposition d’une nouvelle ABI, abi4
  • Gross a créé un prototype de la nouvelle ABI
  • Viktorin est globalement d’accord avec l’approche, mais estime que les détails doivent encore être clarifiés
  • Stinner estime qu’une PEP sur abi4 est nécessaire, et Viktorin considère qu’il s’agit d’une discussion pré-PEP
  • Il existe une confusion autour des garanties de compatibilité offertes par la combinaison entre les versions de l’API C limitée et abi3, ce qui influe aussi sur l’orientation d’abi4
  • Les investigations liées se poursuivent, et une discussion en personne pourrait avoir lieu lors du core developer sprint de mi-octobre

Formulation finale de l’approbation et impact à long terme

  • Les travaux sur CPython sans GIL ou free-threaded se poursuivent, mais l’approbation finale de la PEP 703 est encore en attente
  • Le retard s’est quelque peu prolongé, mais la PEP 703 et ses effets pourraient avoir un impact majeur sur le développement de CPython et son écosystème pendant les cinq prochaines années ou plus
  • Le steering council cherche à clarifier les critères d’approbation
  • Thomas Wouters a indiqué qu’il peaufinait la formulation exacte de l’approbation et cherchait à clarifier plusieurs décisions
  • Une partie du travail pourrait également avancer lors du core developer sprint

1 commentaires

 
GN⁺ 2023-10-22
Avis sur Hacker News
  • Quand on regarde les ordinateurs modernes, on se dit que la parallélisation explicite pourrait devenir un élément plus fondamental de l’informatique que ce que les manuels laissent penser
    C’est peut-être le moment où il faut désormais toujours écrire du code parallèle de façon explicite

    • Comme les humains raisonnent mal sur plusieurs threads à la fois, le changement le plus pratique ira peut-être plutôt du côté des syntaxes déclaratives déjà visibles
      Par exemple, les boucles for sont remplacées par des opérations comme foreach, map, filter. Ces expressions indiquent au compilateur/interpréteur l’intention d’appliquer une opération à tous les éléments d’une structure de données, et laissent au compilateur/runtime le soin de décider si et comment paralléliser
    • La parallélisation s’est divisée en plusieurs directions
      Dans l’exécution de services web, chaque requête est suffisamment rapide en soi, et le vrai gain de la parallélisation consiste à traiter de nombreuses requêtes en parallèle. C’est là que le No-GIL s’insère bien
      Lorsqu’il y a beaucoup de sous-requêtes dans une même requête, on les traite souvent avec du code asynchrone, mais c’est fréquemment moins pour le gain de performance de l’asynchrone que parce que créer des threads coûte cher ou que les pools de threads sont pénibles à gérer. L’asynchrone est bon pour le débit mais mauvais pour la latence, et quand on parallélise des requêtes de service, c’est en général la latence qui préoccupe davantage. L’asynchrone l’a surtout emporté pour des raisons d’ergonomie
      Une autre forme de parallélisation apparaît dans les gros traitements offline. C’est le cas de MapReduce ou de Presto, et cela ressemble généralement à des problèmes de type diviser pour régner. L’entraînement de modèles sur GPU est assez similaire
      Ce qui ne s’est pas produit, ce sont les algorithmes locaux hautement parallèles. Dans les services web, la taille des données est petite, donc le gain en latence est faible, l’implémentation est complexe et le coût de coordination entre threads devient élevé. Une petite exception concerne les algorithmes vectorisés, mais ils s’exécutent sur un seul cœur, sans surcharge de coordination, et l’inférence en ligne est elle aussi à nouveau très fortement vectorisée
    • En informatique, la parallélisation ressemble un peu à la sécurité. On sait qu’elle est importante en théorie, mais pour vraiment bien l’apprendre, il faut chercher une formation spécifique
      Avec le temps, les deux s’améliorent. De même que davantage de langages et de bibliothèques deviennent sûrs par défaut, de plus en plus de choses sont désormais parallèles par défaut. Il reste encore du chemin, mais c’est peut-être une bonne chose que cela ne soit pas arrivé trop tôt. La technologie s’est beaucoup améliorée ces dix dernières années
      On peut par exemple comparer ce qu’on peut faire en toute sécurité avec Rayon en Rust et ce qu’on faisait de manière non sûre avec OpenMP en C++
      Plus à l’extérieur, il y a aussi ce genre de choses sur lesquelles je travaille : https://legion.stanford.edu/, https://regent-lang.org/, https://github.com/nv-legate/cunumeric
    • Je vois la parallélisation comme la gestion mémoire. La plupart des programmes que nous écrivons peuvent, et devraient, utiliser une forme de gestion automatique, et la gestion manuelle peut rester limitée aux zones où elle est nécessaire pour les performances
      Comme il s’agit d’un détail d’implémentation, s’il est possible de l’abstraire pour la rendre plus facile à exploiter, il faut le faire
    • Le wiki de LMAX Disruptor indique qu’envoyer un message d’un thread à un autre a une latence moyenne de 53 nanosecondes
      À titre de comparaison, un mutex tourne autour de 25 nanosecondes, et davantage en cas de contention, mais un mutex reste une synchronisation point à point
      Ce qui est bien avec Disruptor, c’est que plusieurs threads peuvent recevoir le même message sans gros effort supplémentaire
      https://github.com/LMAX-Exchange/disruptor/wiki/Performance-...
      https://gist.github.com/rmacy/2879257
      Je rêve d’un langage un peu comme Smalltalk, mais qui reste mono-thread tant que la parallélisation n’a pas de sens
      Je cherche des problèmes de parallélisation qui ne relèvent pas du big data. La parallélisation ressemble plus au fait de mettre plus de voitures sur la route qu’à augmenter la vitesse d’une voiture. Mais je cherche encore ce que les utilisateurs desktop ou mobile ont réellement besoin de faire en local pour exploiter la puissance de calcul mathématique de leur machine
      Je réfléchis aussi à des idées de parallélisation comme Itanium et les architectures VLIW
  • Il suffit d’utiliser -ng, au sens de no-gil ou next-generation

    • Cela rappelle la grande vague du support des threads Unix. Ce que les développeurs devaient faire variait énormément selon les plateformes
      Il y avait de nouveaux flags de compilation, de nouveaux flags d’édition de liens, des liens vers des bibliothèques différentes, voire l’utilisation d’une commande de compilation complètement différente. AIX était particulièrement comme ça
  • Pour le problème du shebang, il vaudrait mieux s’appuyer sur les conventions Python existantes : from __future__ import nogil
    Il suffirait ensuite de faire un hot swap de l’interpréteur à ce moment-là

    • from __future__ import n’est pas une instruction d’exécution, mais une instruction spéciale qui représente un drapeau
      https://docs.python.org/3/reference/simple_stmts.html#future...
    • « Une future statement est une directive au compilateur demandant que le module concerné soit compilé avec la syntaxe ou la sémantique qui sera disponible dans une future version de Python où cette fonctionnalité deviendra standard »
      Les future statements fonctionnent module par module, et GIL/no-GIL ne s’intègre pas facilement à ce modèle
    • Si ce n’est pas le premier module exécuté et le tout premier import, l’implémentation peut virer au cauchemar
  • Chaque fois que je vois cette proposition, je me demande comment on peut garantir que les programmes continueront à fonctionner correctement. Une grande partie du code Python multithread existant est écrite de manière non sûre
    Le problème, en particulier, ce sont les data races, que j’ai vues à répétition dans les bases de code de plusieurs entreprises et dans des projets open source
    Si ces programmes ne cassent pas, c’est uniquement parce qu’ils reposent implicitement sur le fait que le GIL n’autorise l’exécution que d’un seul thread à la fois
    Si le GIL disparaît, ces programmes casseront. Python étant un langage à typage dynamique, je doute fortement qu’il puisse exister un analyseur statique capable de repérer ce genre de problème dans les programmes Python existants
    Le plus probable, ce sont plutôt des bugs subtils qui apparaîtront de manière non déterministe à l’exécution. Un crash serait presque préférable, mais ce type de bug a de fortes chances d’aboutir à un comportement erroné
    Peut-être aussi que cette proposition sans GIL n’est pas destinée à la majorité des programmes. Ce sera peut-être un outil ultra-spécialisé, réservé à de très rares cas où le programmeur sait qu’il n’y a pas de GIL et écrit son code en conséquence

    • Un programme multithread avec des data races a déjà un problème. Le GIL ne rend pas les data races impossibles
      Le GIL signifie simplement qu’un seul thread peut exécuter du bytecode Python à la fois. Même avec un interpréteur doté du GIL, on peut changer de thread entre des bytecodes, et de nombreuses opérations Python nécessitent plusieurs bytecodes. Cela inclut aussi des méthodes intégrées de types intégrés que beaucoup de gens considèrent comme « atomiques »
      C’est pour cela que Python fournit déjà des verrous, mutex, sémaphores, etc., malgré la présence actuelle du GIL
    • Petit fait amusant : le GIL n’empêche absolument pas tous les bugs de condition de concurrence
      Des threads qui se disputent le GIL peuvent déjà se le reprendre mutuellement au pire moment et semer le chaos
    • Si j’ai bien compris, le point clé est que les bibliothèques déclarent si elles prennent en charge le mode nogil, donc sur un principe d’opt-in
      Si un programme ne s’exécute sans GIL que lorsque toutes ses dépendances l’autorisent, il y aura largement le temps de corriger ce genre de bugs
    • Le Python sans GIL n’arrivera sans doute qu’après au moins 3 ou 4 cycles de release. Cela fait un an que la 3.11 est sortie, et j’ai l’impression qu’en production on voit encore énormément de code Python en 3.8
      Donc le moment où ce problème devra être traité à grande échelle n’arrivera probablement pas avant qu’on approche de 2030. On ne voit déjà pas beaucoup d’environnements de production qui passent immédiatement à la dernière release du runtime
      Je ne veux pas paraître brutal, mais le Steering Council a dit qu’il ne voulait pas d’une nouvelle migration du type 2 vers 3, donc les gens ne mettront pas à jour à la légère. Une grande partie de ce qu’on trouve aujourd’hui en ligne pourrait devenir dangereuse à copier-coller
    • Le GIL ne protège que l’interpréteur. Tout ce qu’il peut faire, c’est réduire la fréquence d’apparition des problèmes
      En pratique, le code Python contient énormément de bugs de threading
  • OCaml n’a-t-il pas connu une évolution similaire ? Je me demande s’il y a des comparaisons pertinentes à faire entre les deux projets

    • Je ne pense pas. OCaml 5, au lieu de supprimer un verrou global en cassant le code existant, a introduit une nouvelle primitive appelée domain, qui gère un ou plusieurs threads partageant un verrou
      Ainsi, l’API de threads existante crée des threads dans le domain courant et permet d’isoler le code qui s’attend à prendre le verrou. Le nouveau code peut à la place créer un nouveau domain démarrant avec un seul thread. On peut aussi utiliser volontairement les deux ensemble comme forme d’ordonnancement
      Python, lui, cherche à rendre le verrou global entièrement optionnel, de manière globale et hors du contrôle des auteurs de bibliothèques. Cela dit, le verrou de Python semble être garanti comme ne protégeant que le runtime lui-même, donc la plupart du code qui en dépend a probablement déjà des bugs, ce qui rend aussi le plan de Python plausible
      S’il y a un point commun, ce serait surtout le fait de devoir repérer et corriger des états partagés inattendus dans toute la base de code du runtime, et de devoir réviser l’ABI C
  • Python a maintenant une chance de rattraper Tcl en performances multithread : https://www.hammerdb.com/blog/uncategorized/why-tcl-is-700-f...

  • Je préférerais encore porter du code Python vers Mojo pour obtenir du multithreading, du SIMD et d’autres gains de performance

    • J’aimerais bien que Mojo soit dans un état plus abouti, mais on en est vraiment très loin aujourd’hui
    • D’accord. Je réécrirais plutôt ça en Rust, Nim ou .NET
    • Les downvotes jusqu’à -3 montrent bien que les downvotes sur HN ne relèvent pas de la logique, mais de luttes de territoire et d’ego
      Tu ne veux pas porter ton code Python vers un Python nogil ? Alors on te downvote, en gros
  • Si on devait lui donner un nom, on pourrait avoir python4, python3-gilfoil, python3-gilfree, etc.

  • Je trouve assez étrange l’élan actuel qui consiste à se concentrer sur Python sans GIL. L’équipe Faster CPython s’était fixé l’objectif ambitieux d’augmenter de 50 % les performances de CPython à chaque release
    Il y a bien eu de vraies améliorations en 3.11, mais on était encore très loin des 50 %, et dans une grande partie de nos tests, la 3.12 était similaire ou plus lente. Le vrai multithreading serait formidable, mais je préférerais de loin voir d’abord s’améliorer les performances en mono-thread
    Bien sûr, je reconnais que nos besoins ne représentent pas forcément tout le monde, et je remercie pour tout le travail accompli pour faire de Python un excellent langage. Cela dit, je me demande quand même ce qui m’échappe

    • Je pense que Python doit avoir une réponse urgente à apporter sur l’utilisation de plusieurs cœurs. AMD vient justement de sortir un CPU 96 cœurs
      Aujourd’hui, l’utilisation de plusieurs cœurs passe par multiprocessing, avec beaucoup de limites. Je comprends que plusieurs interpréteurs puissent arriver avec des choses du genre goroutines, mais je préfère malgré tout une vraie option de multithreading
    • Ce sont deux objectifs complètement différents. En théorie, un Python multithread peut accélérer certains programmes, mais la manière compte
      Dans nogil Python, par exemple, plusieurs threads peuvent appeler du code C avec un état partagé accessible via des objets Python. C’est assez central pour le machine learning, et dans les faits, la forme actuelle de cette PEP vient de l’équipe PyTorch
      Les performances en mono-thread comptent aussi, mais pour les sections critiques, il existait déjà des solutions de contournement plutôt correctes comme numba, Cython et Mojo
      L’ordre a aussi son importance. Si nogil est introduit, une bonne partie du travail de Faster CPython pourrait être entièrement abandonnée, donc les équipes ont dû se coordonner
      Dans un monde idéal, on aurait à la fois un mode nogil et des améliorations des performances en mono-thread. Guido a aussi laissé entendre qu’il envisageait un JIT sophistiqué
    • Les parties coûteuses en calcul de « Python » sont traitées dans des bibliothèques comme numpy et tensorflow
      Python rend très pratique la manipulation d’abstractions bas niveau dans un langage de plus haut niveau. C’est pourquoi, en tant que développeur Python de longue date, je n’ai jamais été particulièrement stressé par le GIL
    • Je pense que vous passez à côté du fait que les besoins des personnes citées dans la PEP 703 et les vôtres sont différents : <https://peps.python.org/pep-0703/#motivation>
    • Si je comprends bien, ce sont des projets distincts à l’intérieur de CPython, et ce ne sont pas forcément les mêmes personnes qui travaillent dessus
      S’il fallait n’en choisir qu’un, je suis d’accord pour dire que, pour la plupart des cas d’usage, du code mono-thread simplement plus rapide conviendrait mieux. Mais il n’y a pas non plus de raison de ne pas avoir les deux
  • Avec le recul, c’est évident, mais si l’équipe Python avait su à quel point la transition de la 2 vers la 3 serait longue et douloureuse, elle aurait sans doute beaucoup plus remanié l’intérieur de l’interpréteur dès le départ
    Après une transition qui a duré 12 ans, les performances en mono-thread restent médiocres, et il reste encore plusieurs transitions douloureuses avant d’arriver à un vrai multithreading
    Je sais qu’il faut faire preuve de bienveillance envers le développement open source, mais à partir d’un certain point, je me demande s’il n’est pas permis de parler d’un langage très mal géré

    • Ce n’est pas mal géré. Python a beaucoup de problèmes, mais ils découlent tous du succès de Python
      Les pires aspects de Python sont ceux qu’il est difficile de changer précisément parce que Python est trop populaire et que son écosystème est trop vaste. Du coup, tout type de changement devient plus difficile à cause de la compatibilité descendante
    • C’est vrai. Après tout ce temps, multiprocessing reste toujours médiocre
      Il y a une tendance à défendre Python trop vite. Il est important de l’examiner objectivement, sans biais
    • À un moment donné, ne faudrait-il pas créer un langage avec la même syntaxe que Python, mais conçu dès le départ pour de meilleures performances et une meilleure prise en charge des threads ?
      Les projets qui veulent des performances et la syntaxe Python pourraient aller de ce côté-là. En l’état, Python semble se débattre entre plusieurs objectifs sans vraiment en atteindre aucun correctement
    • Le fait que la transition de la 2 vers la 3 serait longue et pénible était prévisible, et les gens le disaient déjà à l’époque
      Perl 5/6 était cité en exemple. Même une fois qu’il est devenu évident que personne ne migrerait, il a encore fallu environ 5 ans avant qu’on essaie de rendre la transition plus facile